chromium/ash/picker/search/picker_date_search.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ash/picker/search/picker_date_search.h"

#include <map>
#include <optional>
#include <string>
#include <vector>

#include "ash/public/cpp/picker/picker_search_result.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/containers/fixed_flat_map.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/time_formatting.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"

namespace ash {
namespace {

constexpr int kDaysPerWeek = 7;
constexpr LazyRE2 kDaysOrWeeksAwayRegex = {
    "(\\d{1,6}|one|two|three|four|five|six|seven|eight|nine|ten) "
    "(days?|weeks?) (from now|ago)"};
constexpr LazyRE2 kDayOfWeekRegex = {
    "(this |next |last |)"
    "(sunday|monday|tuesday|wednesday|thursday|friday|saturday)"};
constexpr auto kTextToDays = base::MakeFixedFlatMap<std::string_view, int>({
    {"yesterday", -1},
    {"today", 0},
    {"tomorrow", 1},
});
constexpr auto kWordToNumber = base::MakeFixedFlatMap<std::string_view, int>({
    {"one", 1},
    {"two", 2},
    {"three", 3},
    {"four", 4},
    {"five", 5},
    {"six", 6},
    {"seven", 7},
    {"eight", 8},
    {"nine", 9},
    {"ten", 10},
});
constexpr auto kDayOfWeekToNumber =
    base::MakeFixedFlatMap<std::string_view, int>({
        {"sunday", 0},
        {"monday", 1},
        {"tuesday", 2},
        {"wednesday", 3},
        {"thursday", 4},
        {"friday", 5},
        {"saturday", 6},
    });

constexpr std::u16string_view kSuggestedDates[] = {
    {u"Today"},
    {u"Tomorrow"},
    {u"2 weeks from now"},
};

std::u16string GetLocalizedDayOfWeek(const base::Time& time) {
  return base::LocalizedTimeFormatWithPattern(time, "EEEE");
}

// The result of parsing a date expression query.
struct ResolvedDate {
  base::Time time;

  // Some optional text to disambiguate the date when the original query is
  // ambiguous.
  std::optional<std::u16string> disambiguation_text;
};

PickerSearchResult MakeResult(const ResolvedDate& date) {
  return PickerTextResult(
      base::LocalizedTimeFormatWithPattern(date.time, "LLLd"),
      date.disambiguation_text.value_or(u""),
      ui::ImageModel::FromVectorIcon(kPickerCalendarIcon,
                                     cros_tokens::kCrosSysOnSurface),
      PickerTextResult::Source::kDate);
}

PickerSearchResult MakeSuggestedResult(std::u16string_view query_text,
                                       const ResolvedDate& date) {
  CHECK(!date.disambiguation_text.has_value());
  return PickerSearchRequestResult(
      query_text, base::LocalizedTimeFormatWithPattern(date.time, "LLLd"),
      ui::ImageModel::FromVectorIcon(kPickerCalendarIcon,
                                     cros_tokens::kCrosSysOnSurface));
}

void HandleSpecificDayQueries(const base::Time& now,
                              std::string_view query,
                              std::vector<ResolvedDate>& resolved_dates) {
  const auto day_lookup = kTextToDays.find(query);
  if (day_lookup == kTextToDays.end()) {
    return;
  }
  resolved_dates.push_back({.time = now + base::Days(day_lookup->second)});
}

void HandleDaysOrWeeksAwayQueries(const base::Time& now,
                                  std::string_view query,
                                  std::vector<ResolvedDate>& resolved_dates) {
  std::string number, unit, suffix;
  if (!RE2::FullMatch(query, *kDaysOrWeeksAwayRegex, &number, &unit, &suffix)) {
    return;
  }
  const auto word_lookup = kWordToNumber.find(number);
  int x = 0;
  if (word_lookup != kWordToNumber.end()) {
    x = word_lookup->second;
  } else {
    base::StringToInt(number, &x);
  }
  if (x <= 0) {
    return;
  }
  if (unit.starts_with("week")) {
    x *= kDaysPerWeek;
  }
  if (suffix == "ago") {
    x = -x;
  }
  resolved_dates.push_back({.time = now + base::Days(x)});
}

void HandleDayOfWeekQueries(const base::Time& now,
                            std::string_view query,
                            std::vector<ResolvedDate>& resolved_dates) {
  std::string prefix, target_day_of_week_str;
  if (!RE2::FullMatch(query, *kDayOfWeekRegex, &prefix,
                      &target_day_of_week_str)) {
    return;
  }
  const auto day_lookup = kDayOfWeekToNumber.find(target_day_of_week_str);
  CHECK(day_lookup != kDayOfWeekToNumber.end());
  int target_day_of_week = day_lookup->second;
  base::Time::Exploded exploded;
  now.LocalExplode(&exploded);
  int current_day_of_week = exploded.day_of_week;
  int day_diff = target_day_of_week - current_day_of_week;
  if (prefix.empty() || prefix == "this ") {
    if (target_day_of_week < current_day_of_week) {
      std::u16string localized_day_of_week =
          GetLocalizedDayOfWeek(now + base::Days(day_diff));
      resolved_dates.push_back({
          .time = now + base::Days(day_diff + kDaysPerWeek),
          .disambiguation_text = l10n_util::GetStringFUTF16(
              IDS_PICKER_DATE_DISAMBIGUATION_THIS_COMING_DAY,
              localized_day_of_week),
      });
      resolved_dates.push_back({
          .time = now + base::Days(day_diff),
          .disambiguation_text = l10n_util::GetStringFUTF16(
              IDS_PICKER_DATE_DISAMBIGUATION_THIS_PAST_DAY,
              std::move(localized_day_of_week)),
      });
    } else {
      resolved_dates.push_back({.time = now + base::Days(day_diff)});
    }
  } else if (prefix == "next ") {
    if (target_day_of_week > current_day_of_week) {
      std::u16string localized_day_of_week =
          GetLocalizedDayOfWeek(now + base::Days(day_diff));
      resolved_dates.push_back({
          .time = now + base::Days(day_diff + kDaysPerWeek),
          .disambiguation_text = l10n_util::GetStringFUTF16(
              IDS_PICKER_DATE_DISAMBIGUATION_NEXT_WEEK, localized_day_of_week),
      });
      resolved_dates.push_back({
          .time = now + base::Days(day_diff),
          .disambiguation_text = l10n_util::GetStringFUTF16(
              IDS_PICKER_DATE_DISAMBIGUATION_THIS_COMING_DAY,
              std::move(localized_day_of_week)),
      });
    } else {
      resolved_dates.push_back(
          {.time = now + base::Days(day_diff + kDaysPerWeek)});
    }
  } else if (prefix == "last ") {
    if (target_day_of_week < current_day_of_week) {
      std::u16string localized_day_of_week =
          GetLocalizedDayOfWeek(now + base::Days(day_diff));
      resolved_dates.push_back({
          .time = now + base::Days(day_diff - kDaysPerWeek),
          .disambiguation_text = l10n_util::GetStringFUTF16(
              IDS_PICKER_DATE_DISAMBIGUATION_LAST_WEEK, localized_day_of_week),
      });
      resolved_dates.push_back({
          .time = now + base::Days(day_diff),
          .disambiguation_text = l10n_util::GetStringFUTF16(
              IDS_PICKER_DATE_DISAMBIGUATION_THIS_PAST_DAY,
              std::move(localized_day_of_week)),
      });
    } else {
      resolved_dates.push_back(
          {.time = now + base::Days(day_diff - kDaysPerWeek)});
    }
  }
}

std::vector<ResolvedDate> ResolveQuery(const base::Time& now,
                                       std::u16string_view query) {
  std::vector<ResolvedDate> resolved_dates;
  std::string clean_query = base::UTF16ToUTF8(base::TrimWhitespace(
      base::i18n::ToLower(query), base::TrimPositions::TRIM_ALL));
  HandleSpecificDayQueries(now, clean_query, resolved_dates);
  HandleDaysOrWeeksAwayQueries(now, clean_query, resolved_dates);
  HandleDayOfWeekQueries(now, clean_query, resolved_dates);
  return resolved_dates;
}

}  // namespace

std::vector<PickerSearchResult> PickerDateSearch(const base::Time& now,
                                                 std::u16string_view query) {
  std::vector<ResolvedDate> resolved_dates = ResolveQuery(now, query);
  std::vector<PickerSearchResult> results;
  results.reserve(resolved_dates.size());
  for (const ResolvedDate& resolved_date : resolved_dates) {
    results.push_back(MakeResult(resolved_date));
  }
  return results;
}

std::vector<PickerSearchResult> PickerSuggestedDateResults() {
  std::vector<PickerSearchResult> results;

  for (const std::u16string_view query : kSuggestedDates) {
    std::vector<ResolvedDate> resolved_dates =
        ResolveQuery(base::Time::Now(), query);
    CHECK_EQ(resolved_dates.size(), 1u);
    results.push_back(MakeSuggestedResult(query, resolved_dates[0]));
  }

  return results;
}

}  // namespace ash