| // 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. |
| |
| #include "ash/quick_insert/search/quick_insert_date_search.h" |
| |
| #include <map> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "ash/quick_insert/quick_insert_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/containers/to_vector.h" |
| #include "base/i18n/case_conversion.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/strings/string_number_conversions.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; |
| }; |
| |
| QuickInsertSearchResult MakeResult(const ResolvedDate& date) { |
| return QuickInsertTextResult( |
| base::LocalizedTimeFormatWithPattern(date.time, "LLLd"), |
| date.disambiguation_text.value_or(u""), |
| ui::ImageModel::FromVectorIcon(kQuickInsertCalendarIcon, |
| cros_tokens::kCrosSysOnSurface), |
| QuickInsertTextResult::Source::kDate); |
| } |
| |
| QuickInsertSearchResult MakeSuggestedResult(std::u16string_view query_text, |
| const ResolvedDate& date) { |
| CHECK(!date.disambiguation_text.has_value()); |
| return QuickInsertSearchRequestResult( |
| query_text, base::LocalizedTimeFormatWithPattern(date.time, "LLLd"), |
| ui::ImageModel::FromVectorIcon(kQuickInsertCalendarIcon, |
| 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<QuickInsertSearchResult> QuickInsertDateSearch( |
| const base::Time& now, |
| std::u16string_view query) { |
| return base::ToVector(ResolveQuery(now, query), MakeResult); |
| } |
| |
| std::vector<QuickInsertSearchResult> QuickInsertSuggestedDateResults() { |
| std::vector<QuickInsertSearchResult> 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 |