chromium/ash/system/time/date_helper.cc

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

#include "ash/system/time/date_helper.h"

#include "ash/shell.h"
#include "ash/system/locale/locale_update_controller_impl.h"
#include "ash/system/model/clock_model.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_utils.h"
#include "base/containers/contains.h"
#include "base/i18n/unicodestring.h"
#include "base/memory/ptr_util.h"
#include "base/time/time.h"
#include "third_party/icu/source/common/unicode/dtintrv.h"
#include "third_party/icu/source/i18n/unicode/dtitvfmt.h"
#include "third_party/icu/source/i18n/unicode/fieldpos.h"
#include "third_party/icu/source/i18n/unicode/gregocal.h"

namespace ash {

namespace {

// Milliseconds per minute.
constexpr int kMillisecondsPerMinute = 60000;

// Default week title for a few special languages that cannot find the start of
// a week. So far the known languages that cannot return their day of week are:
// 'bn', 'fa', 'mr', 'pa-PK'.
const std::vector<std::u16string> kDefaultWeekTitle = {u"S", u"M", u"T", u"W",
                                                       u"T", u"F", u"S"};

UDate TimeToUDate(const base::Time& time) {
  return static_cast<UDate>(time.InSecondsFSinceUnixEpoch() *
                            base::Time::kMillisecondsPerSecond);
}

// Receives an input `unicode_pattern` in the "Hm" format (HH:mm, aK:mm, h:mm a,
// a hh:mm, etc.) and extracts the hours part of the pattern.
icu::UnicodeString getHoursPattern(const icu::UnicodeString& unicode_pattern) {
  std::string pattern;
  unicode_pattern.toUTF8String(pattern);

  if (base::Contains(pattern, "hh")) {
    return icu::UnicodeString("hh");
  }
  if (base::Contains(pattern, "h")) {
    return icu::UnicodeString("h");
  }
  if (base::Contains(pattern, "HH")) {
    return icu::UnicodeString("HH");
  }
  if (base::Contains(pattern, "H")) {
    return icu::UnicodeString("H");
  }
  if (base::Contains(pattern, "KK")) {
    return icu::UnicodeString("KK");
  }
  if (base::Contains(pattern, "K")) {
    return icu::UnicodeString("K");
  }
  if (base::Contains(pattern, "kk")) {
    return icu::UnicodeString("kk");
  }
  if (base::Contains(pattern, "k")) {
    return icu::UnicodeString("k");
  }

  NOTREACHED() << "Hours pattern not found.";
}

}  // namespace

// static
DateHelper* DateHelper::GetInstance() {
  return base::Singleton<DateHelper>::get();
}

icu::SimpleDateFormat DateHelper::CreateSimpleDateFormatter(
    const char* pattern) {
  // Generate a locale-dependent format pattern. The generator will take
  // care of locale-dependent formatting issues like which separator to
  // use (some locales use '.' instead of ':'), and where to put the am/pm
  // marker.
  UErrorCode status = U_ZERO_ERROR;
  DCHECK(U_SUCCESS(status));
  std::unique_ptr<icu::DateTimePatternGenerator> generator(
      icu::DateTimePatternGenerator::createInstance(status));
  DCHECK(U_SUCCESS(status));
  icu::UnicodeString generated_pattern =
      generator->getBestPattern(icu::UnicodeString(pattern), status);
  DCHECK(U_SUCCESS(status));

  // Then, create a formatter object using the generated pattern.
  icu::SimpleDateFormat formatter(generated_pattern, status);
  DCHECK(U_SUCCESS(status));

  return formatter;
}

icu::SimpleDateFormat DateHelper::CreateSimpleDateFormatterWithoutBestPattern(
    const char* pattern) {
  UErrorCode status = U_ZERO_ERROR;
  DCHECK(U_SUCCESS(status));
  icu::SimpleDateFormat formatter(icu::UnicodeString(pattern), status);
  DCHECK(U_SUCCESS(status));
  return formatter;
}

std::unique_ptr<icu::DateIntervalFormat>
DateHelper::CreateDateIntervalFormatter(const char* pattern) {
  UErrorCode status = U_ZERO_ERROR;
  icu::DateIntervalFormat* formatter =
      icu::DateIntervalFormat::createInstance(pattern, status);
  DCHECK(U_SUCCESS(status));
  return base::WrapUnique(formatter);
}

icu::SimpleDateFormat DateHelper::CreateHoursFormatter(const char* pattern) {
  UErrorCode status = U_ZERO_ERROR;
  DCHECK(U_SUCCESS(status));
  std::unique_ptr<icu::DateTimePatternGenerator> generator(
      icu::DateTimePatternGenerator::createInstance(status));
  DCHECK(U_SUCCESS(status));
  icu::UnicodeString generated_pattern =
      generator->getBestPattern(icu::UnicodeString(pattern), status);
  DCHECK(U_SUCCESS(status));
  // Since ICU 74, getBestPattern can return a gibberish pattern ""H
  // ├'Minute': m┤ ├'Dayperiod': a┤"" if the locale resource is missing. Instead
  // of using the gibberish pattern, this should fallback to the proposed
  // pattern.
  std::string gen_string;
  generated_pattern.toUTF8String(gen_string);
  if (base::Contains(gen_string, "├")) {
    // Fallback to the suggested pattern.
    generated_pattern = icu::UnicodeString(pattern);
  }
  // Extract the hours from the generated pattern.
  icu::UnicodeString hours_pattern = getHoursPattern(generated_pattern);
  icu::SimpleDateFormat formatter(hours_pattern, status);
  DCHECK(U_SUCCESS(status));

  return formatter;
}

std::u16string DateHelper::GetFormattedTime(const icu::DateFormat* formatter,
                                            const base::Time& time) {
  DCHECK(formatter);
  icu::UnicodeString date_string;

  formatter->format(TimeToUDate(time), date_string);
  return base::i18n::UnicodeStringToString16(date_string);
}

std::u16string DateHelper::GetFormattedInterval(
    const icu::DateIntervalFormat* formatter,
    const base::Time& start_time,
    const base::Time& end_time) {
  DCHECK(formatter);
  UErrorCode status = U_ZERO_ERROR;
  icu::DateInterval interval(TimeToUDate(start_time), TimeToUDate(end_time));
  icu::FieldPosition position = 0;
  icu::UnicodeString interval_string;
  formatter->format(&interval, interval_string, position, status);
  DCHECK(U_SUCCESS(status));
  return base::i18n::UnicodeStringToString16(interval_string);
}

base::TimeDelta DateHelper::GetTimeDifference(base::Time date) const {
  const icu::TimeZone& time_zone =
      system::TimezoneSettings::GetInstance()->GetTimezone();
  const base::TimeDelta raw_time_diff =
      base::Minutes(time_zone.getRawOffset() / kMillisecondsPerMinute);

  // Calculates the time difference adjust by the possible daylight savings
  // offset. If the status of any step fails, returns the default time
  // difference without considering daylight savings.
  if (!gregorian_calendar_) {
    return raw_time_diff;
  }

  UDate current_date = TimeToUDate(date);
  UErrorCode status = U_ZERO_ERROR;
  gregorian_calendar_->setTime(current_date, status);
  if (U_FAILURE(status)) {
    return raw_time_diff;
  }

  status = U_ZERO_ERROR;
  UBool day_light = gregorian_calendar_->inDaylightTime(status);
  if (U_FAILURE(status)) {
    return raw_time_diff;
  }

  int gmt_offset = time_zone.getRawOffset();
  if (day_light) {
    gmt_offset += time_zone.getDSTSavings();
  }

  return base::Minutes(gmt_offset / kMillisecondsPerMinute);
}

base::Time DateHelper::GetLocalMidnight(base::Time date) {
  base::TimeDelta time_difference = GetTimeDifference(date);
  return (date + time_difference).UTCMidnight() - time_difference;
}

DateHelper::DateHelper()
    : day_of_month_formatter_(CreateSimpleDateFormatter("d")),
      month_day_formatter_(CreateSimpleDateFormatter("MMMMd")),
      month_day_year_formatter_(CreateSimpleDateFormatter("MMMMdyyyy")),
      month_day_year_week_formatter_(
          CreateSimpleDateFormatter("MMMMEEEEdyyyy")),
      month_name_formatter_(CreateSimpleDateFormatter("MMMM")),
      month_name_year_formatter_(CreateSimpleDateFormatter("MMMM yyyy")),
      time_zone_formatter_(CreateSimpleDateFormatter("zzzz")),
      twelve_hour_clock_formatter_(CreateSimpleDateFormatter("h:mm a")),
      twenty_four_hour_clock_formatter_(CreateSimpleDateFormatter("HH:mm")),
      day_of_week_formatter_(CreateSimpleDateFormatter("ee")),
      week_title_formatter_(CreateSimpleDateFormatter("EEEEE")),
      // Note: "yyyy" represents a four-digit calendar year (e.g. "2023"),
      // while "YYYY" represents a so called 'week year' (which might be "2022"
      // if the first day is on the last week of 2022).
      year_formatter_(CreateSimpleDateFormatter("yyyy")),
      twelve_hour_clock_hours_formatter_(CreateHoursFormatter("h:mm a")),
      twenty_four_hour_clock_hours_formatter_(CreateHoursFormatter("HH:mm")),
      minutes_formatter_(CreateSimpleDateFormatterWithoutBestPattern("mm")),
      twelve_hour_clock_interval_formatter_(CreateDateIntervalFormatter("hm")),
      twenty_four_hour_clock_interval_formatter_(
          CreateDateIntervalFormatter("Hm")) {
  const icu::TimeZone& time_zone =
      system::TimezoneSettings::GetInstance()->GetTimezone();

  UErrorCode status = U_ZERO_ERROR;
  gregorian_calendar_ =
      std::make_unique<icu::GregorianCalendar>(time_zone, status);
  DCHECK(U_SUCCESS(status));
  CalculateLocalWeekTitles();
  time_zone_settings_observer_.Observe(system::TimezoneSettings::GetInstance());

  // Not using a scoped observer since the Shell can be destructed before this
  // `DateHelper` instance gets destructed.
  Shell::Get()->locale_update_controller()->AddObserver(this);
}

DateHelper::~DateHelper() {
  if (Shell::HasInstance()) {
    Shell::Get()->locale_update_controller()->RemoveObserver(this);
  }
}

void DateHelper::ResetFormatters() {
  day_of_month_formatter_ = CreateSimpleDateFormatter("d");
  month_day_formatter_ = CreateSimpleDateFormatter("MMMMd");
  month_day_year_formatter_ = CreateSimpleDateFormatter("MMMMdyyyy");
  month_day_year_week_formatter_ = CreateSimpleDateFormatter("MMMMEEEEdyyyy");
  month_name_formatter_ = CreateSimpleDateFormatter("MMMM");
  month_name_year_formatter_ = CreateSimpleDateFormatter("MMMM yyyy");
  time_zone_formatter_ = CreateSimpleDateFormatter("zzzz");
  twelve_hour_clock_formatter_ = CreateSimpleDateFormatter("h:mm a");
  twenty_four_hour_clock_formatter_ = CreateSimpleDateFormatter("HH:mm");
  day_of_week_formatter_ = CreateSimpleDateFormatter("ee");
  week_title_formatter_ = CreateSimpleDateFormatter("EEEEE");
  year_formatter_ = CreateSimpleDateFormatter("yyyy");
  twelve_hour_clock_hours_formatter_ = CreateHoursFormatter("h:mm a");
  twenty_four_hour_clock_hours_formatter_ = CreateHoursFormatter("HH:mm");
  minutes_formatter_ = CreateSimpleDateFormatterWithoutBestPattern("mm");
  twelve_hour_clock_interval_formatter_ = CreateDateIntervalFormatter("hm");
  twenty_four_hour_clock_interval_formatter_ =
      CreateDateIntervalFormatter("Hm");
}

void DateHelper::ResetForTesting() {
  ResetFormatters();
  CalculateLocalWeekTitles();
  gregorian_calendar_->setTimeZone(
      system::TimezoneSettings::GetInstance()->GetTimezone());
}

void DateHelper::CalculateLocalWeekTitles() {
  week_titles_.clear();

  // To avoid the DST difference, use a certain date here to calculate the week
  // titles, since there are no daylight saving starts/ends in June worldwide.
  // If the `DCHECK` fails, use `Now()`.
  base::Time start_date = base::Time::Now();
  bool result = base::Time::FromString("15 Jun 2021 10:00 GMT", &start_date);
  DCHECK(result);
  start_date = GetLocalMidnight(start_date);
  std::u16string day_of_week =
      GetFormattedTime(&day_of_week_formatter_, start_date);

  // For a few special locales the day of week is not in a number. In these
  // cases, use the default week titles.
  int day_int;
  if (!base::StringToInt(day_of_week, &day_int)) {
    week_titles_ = kDefaultWeekTitle;
    return;
  }

  int safe_index = 0;
  // Find a first day of a week.
  while (day_int != 1) {
    start_date += base::Hours(25);
    day_of_week = GetFormattedTime(&day_of_week_formatter_, start_date);
    result = base::StringToInt(day_of_week, &day_int);
    DCHECK(result);
    ++safe_index;
    if (safe_index == calendar_utils::kDateInOneWeek) {
      NOTREACHED() << "Should already find the first day within 7 times, since "
                      "there are only 7 days in a week";
    }
  }

  int day_index = 0;
  while (day_index < calendar_utils::kDateInOneWeek) {
    week_titles_.push_back(
        GetFormattedTime(&week_title_formatter_, start_date));
    start_date += base::Hours(25);
    ++day_index;
  }
}

void DateHelper::TimezoneChanged(const icu::TimeZone& timezone) {
  ResetFormatters();
  gregorian_calendar_->setTimeZone(
      system::TimezoneSettings::GetInstance()->GetTimezone());
  Shell::Get()->system_tray_model()->calendar_model()->RedistributeEvents();
  Shell::Get()->system_tray_model()->clock()->NotifyRefreshClock();
}

void DateHelper::OnLocaleChanged() {
  ResetFormatters();
  CalculateLocalWeekTitles();
}

}  // namespace ash