chromium/chromeos/ash/components/report/utils/time_utils.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/report/utils/time_utils.h"

#include <memory>
#include <string_view>

#include "base/i18n/time_formatting.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/policy/weekly_time/time_utils.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"

namespace ash::report::utils {

namespace {

// Record histogram for whether ActivateDate is read and parsed correctly.
void RecordIsActivateDateSet(bool is_set) {
  base::UmaHistogramBoolean("Ash.Report.IsActivateDateSet", is_set);
}

}  // namespace

base::Time ConvertGmtToPt(base::Clock* clock) {
  base::Time gmt_ts = clock->Now();
  DCHECK(gmt_ts != base::Time::UnixEpoch() && gmt_ts != base::Time())
      << "Invalid timestamp ts  = " << gmt_ts;

  int pt_offset;
  bool offset_success = policy::weekly_time_utils::GetOffsetFromTimezoneToGmt(
      "America/Los_Angeles", clock, &pt_offset);

  if (!offset_success) {
    LOG(ERROR) << "Failed to get offset for Pacific Time. "
               << "Returning UTC-8 timezone as default.";
    return gmt_ts - base::Hours(8);
  }

  return gmt_ts - base::Milliseconds(pt_offset);
}

std::optional<base::Time> GetPreviousMonth(base::Time ts) {
  if (ts == base::Time()) {
    LOG(ERROR) << "Timestamp not set = " << ts;
    return std::nullopt;
  }

  base::Time::Exploded exploded;
  ts.UTCExplode(&exploded);

  // Set new time to the first midnight of the previous month.
  // "+ 11) % 12) + 1" wraps the month around if it goes outside 1..12.
  exploded.month = (((exploded.month - 1) + 11) % 12) + 1;
  exploded.year -= (exploded.month == 12);
  exploded.day_of_month = 1;
  exploded.hour = exploded.minute = exploded.second = exploded.millisecond = 0;

  base::Time new_month_ts;
  bool success = base::Time::FromUTCExploded(exploded, &new_month_ts);

  if (!success) {
    LOG(ERROR) << "Failed to get previous month of ts = " << ts;
    return std::nullopt;
  }

  return new_month_ts;
}

std::optional<base::Time> GetNextMonth(base::Time ts) {
  if (ts == base::Time()) {
    LOG(ERROR) << "Timestamp not set = " << ts;
    return std::nullopt;
  }

  base::Time::Exploded exploded;
  ts.UTCExplode(&exploded);

  // Set new time to the first midnight of the next month.
  // "+ 11) % 12) + 1" wraps the month around if it goes outside 1..12.
  exploded.month = (((exploded.month + 1) + 11) % 12) + 1;
  exploded.year += (exploded.month == 1);
  exploded.day_of_month = 1;
  exploded.hour = exploded.minute = exploded.second = exploded.millisecond = 0;

  base::Time new_month_ts;
  bool success = base::Time::FromUTCExploded(exploded, &new_month_ts);

  if (!success) {
    LOG(ERROR) << "Failed to get next month of ts = " << ts;
    return std::nullopt;
  }

  return new_month_ts;
}

std::optional<base::Time> GetPreviousYear(base::Time ts) {
  if (ts == base::Time()) {
    LOG(ERROR) << "Timestamp not set = " << ts;
    return std::nullopt;
  }

  base::Time::Exploded exploded;
  ts.UTCExplode(&exploded);

  // Set new time to the first midnight of the previous year.
  exploded.year -= 1;
  exploded.day_of_month = 1;
  exploded.hour = 0;
  exploded.minute = 0;
  exploded.second = 0;
  exploded.millisecond = 0;

  base::Time new_year_ts;
  bool success = base::Time::FromUTCExploded(exploded, &new_year_ts);

  if (!success) {
    LOG(ERROR) << "Failed to get previous year of ts = " << ts;
    return std::nullopt;
  }

  return new_year_ts;
}

bool IsSameYearAndMonth(base::Time ts1, base::Time ts2) {
  base::Time::Exploded ts1_exploded;
  ts1.UTCExplode(&ts1_exploded);
  base::Time::Exploded ts2_exploded;
  ts2.UTCExplode(&ts2_exploded);
  return (ts1_exploded.year == ts2_exploded.year) &&
         (ts1_exploded.month == ts2_exploded.month);
}

bool IsFirstActiveUnderNDaysAgo(base::Time active_ts,
                                base::Time first_active_week,
                                int num_days) {
  // Checks for the starting point which is num of days before active_ts.
  base::Time starting_point = active_ts - base::Days(num_days);

  // Check if first_active_week is after the starting point
  return first_active_week >= starting_point;
}

std::string FormatTimestampToMidnightGMTString(base::Time ts) {
  return base::UnlocalizedTimeFormatWithPattern(ts, "yyyy-MM-dd 00:00:00.000 z",
                                                icu::TimeZone::getGMT());
}

std::string TimeToYYYYMMDDString(base::Time ts) {
  return base::UnlocalizedTimeFormatWithPattern(ts, "yyyyMMdd",
                                                icu::TimeZone::getGMT());
}

std::string TimeToYYYYMMString(base::Time ts) {
  return base::UnlocalizedTimeFormatWithPattern(ts, "yyyyMM",
                                                icu::TimeZone::getGMT());
}

std::optional<base::Time> GetFirstActiveWeek() {
  std::optional<std::string_view> first_active_week_val =
      system::StatisticsProvider::GetInstance()->GetMachineStatistic(
          system::kActivateDateKey);
  std::string first_active_week_str =
      std::string(first_active_week_val.value_or(kActivateDateKeyNotFound));

  if (first_active_week_str == kActivateDateKeyNotFound) {
    LOG(ERROR)
        << "Failed to retrieve ActivateDate VPD field from machine statistics. "
        << "Leaving |first_active_week_| unset.";
    RecordIsActivateDateSet(false);
    return std::nullopt;
  }

  // Activate date is formatted: "YYYY-WW"
  int delimiter_index = first_active_week_str.find('-');

  const int expected_first_active_week_size = 7;
  const int expected_delimiter_index = 4;
  if (first_active_week_str.size() != expected_first_active_week_size ||
      delimiter_index != expected_delimiter_index) {
    LOG(ERROR) << "ActivateDate was retrieved but is not formatted as YYYY-WW. "
               << "Received string : " << first_active_week_str;
    RecordIsActivateDateSet(false);
    return std::nullopt;
  }

  const int expected_year_size = 4;
  const int expected_weeks_size = 2;

  std::string parsed_year = first_active_week_str.substr(0, expected_year_size);
  std::string parsed_weeks = first_active_week_str.substr(
      expected_delimiter_index + 1, expected_weeks_size);

  if (parsed_year.empty() || parsed_weeks.empty()) {
    LOG(ERROR) << "Failed to parse and convert the first active weeks string "
               << "year and weeks.";
    RecordIsActivateDateSet(false);
    return std::nullopt;
  }

  // Convert parsed year and weeks to int.
  int activate_year, activate_week_of_year;
  bool success_year = base::StringToInt(parsed_year, &activate_year);
  bool success_week = base::StringToInt(parsed_weeks, &activate_week_of_year);

  if (!success_year || !success_week) {
    LOG(ERROR) << "Failed to convert parsed_year or parsed_weeks: "
               << parsed_year << " and " << parsed_weeks;
    RecordIsActivateDateSet(false);
    return std::nullopt;
  }

  auto iso8601_ts =
      utils::Iso8601DateWeekAsTime(activate_year, activate_week_of_year);
  if (!iso8601_ts.has_value()) {
    LOG(ERROR) << "Failed to ISO8601 year and week of year as a timestamp.";
    RecordIsActivateDateSet(false);
    return std::nullopt;
  }

  RecordIsActivateDateSet(true);
  return iso8601_ts.value();
}

std::optional<base::Time> FirstMondayOfISONewYear(int iso_year) {
  // 1. Get week of first Thursday in iso_year.
  // 2. Subtract days to get the first Monday.
  // ISO calendar new year may start 1-3 days before the
  // Gregorian new year or 1-3 days later.

  // Get week of the first Thursday in ISO year.
  base::Time first_thursday_ts;
  base::Time::Exploded first_thursday_exploded = {iso_year, 1, 0, 1,
                                                  0,        0, 0, 0};
  bool success =
      base::Time::FromUTCExploded(first_thursday_exploded, &first_thursday_ts);

  if (!success) {
    LOG(ERROR) << "Failed to explode first day of iso Year = " << iso_year;
    return std::nullopt;
  }

  // Re-create exploded object from first thursday timestamp.
  // This allows us to get an accurate day of week, so that we can
  // determine the first Thursday in iso_year.
  first_thursday_ts.UTCExplode(&first_thursday_exploded);

  // Adjust number of days to get to the first Thursday of year.
  while (first_thursday_exploded.day_of_week != kThursdayDayOfWeekIndex) {
    first_thursday_ts += base::Days(1);

    // Recalculate exploded object.
    first_thursday_ts.UTCExplode(&first_thursday_exploded);
  }

  base::Time first_monday_ts =
      first_thursday_ts -
      base::Days(kThursdayDayOfWeekIndex - kMondayDayOfWeekIndex);

  return first_monday_ts;
}

// The ActivateDate is formatted: YYYY-WW and is generated based on UTC date.
// Returns the first day of the ISO8601 week.
std::optional<base::Time> Iso8601DateWeekAsTime(int activate_year,
                                                int activate_week_of_year) {
  if (activate_year < 0 || activate_week_of_year <= 0 ||
      activate_week_of_year > 53) {
    LOG(ERROR) << "Invalid year or week of year"
               << ". Variable activate_year = " << activate_year
               << ". Variable activate_week_of_year = "
               << activate_week_of_year;
    return std::nullopt;
  }

  std::optional<base::Time> first_monday_iso_year =
      FirstMondayOfISONewYear(activate_year);

  if (!first_monday_iso_year.has_value()) {
    return std::nullopt;
  }

  // Get the number of days to the start of a ISO 8601 week standard period
  // for that year from the years first monday. This is equal to
  // (activate_week_of_year-1) * 7 days.
  int days_in_iso_period = 0;
  days_in_iso_period = (activate_week_of_year - 1) * 7;

  // Add the above two steps to get the start of a ISO 8601 week time.
  return first_monday_iso_year.value() + base::Days(days_in_iso_period);
}

std::string ConvertTimeToISO8601String(base::Time ts) {
  if (ts.is_null()) {
    LOG(ERROR) << "Timestamp ts is not defined correctly. ts = " << ts;
    return std::string();
  }

  // Calculate the year of the given time
  base::Time::Exploded exploded;
  ts.UTCExplode(&exploded);
  int activate_year = exploded.year;

  std::optional<base::Time> first_monday_iso_year =
      FirstMondayOfISONewYear(activate_year);

  if (!first_monday_iso_year.has_value()) {
    return std::string();
  }

  // Assign to the last ISO week of previous year. We use W52 for simplicity.
  // Some years have 53 weeks, but we simply use 52 to avoid complexity here.
  // This captures ts for up to the 3 days after the new year that are apart of
  // the previous year last ISO week.
  if (ts < first_monday_iso_year.value()) {
    return base::NumberToString(activate_year - 1) + "-52";
  }

  std::optional<base::Time> first_monday_iso_year_next =
      FirstMondayOfISONewYear(activate_year + 1);

  if (!first_monday_iso_year_next.has_value()) {
    return std::string();
  }

  // Assign to first ISO week of next year.
  // This captures ts for up to the 3 days before the new year that are apart of
  // the current year first ISO week.
  if (ts >= first_monday_iso_year_next.value()) {
    return base::NumberToString(activate_year + 1) + "-01";
  }

  // Calculate the number of days between the given time and the first
  // Monday of the year
  base::TimeDelta delta = ts - first_monday_iso_year.value();
  int days_difference = delta.InDays();

  // Calculate the ISO 8601 week number
  int activate_week_of_year = days_difference / 7 + 1;

  return base::NumberToString(activate_year) + "-" +
         (activate_week_of_year < 10 ? "0" : "") +
         base::NumberToString(activate_week_of_year);
}

}  // namespace ash::report::utils