| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/common/time_format.h" |
| |
| #include <vector> |
| |
| #include "base/lazy_instance.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/stl_util-inl.h" |
| #include "base/string16.h" |
| #include "base/time.h" |
| #include "base/utf_string_conversions.h" |
| #include "grit/generated_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "unicode/datefmt.h" |
| #include "unicode/locid.h" |
| #include "unicode/plurfmt.h" |
| #include "unicode/plurrule.h" |
| #include "unicode/smpdtfmt.h" |
| |
| using base::Time; |
| using base::TimeDelta; |
| |
| namespace { |
| |
| static const char kFallbackFormatSuffixShort[] = "}"; |
| static const char kFallbackFormatSuffixLeft[] = " left}"; |
| static const char kFallbackFormatSuffixAgo[] = " ago}"; |
| |
| // Contains message IDs for various time units and pluralities. |
| struct MessageIDs { |
| // There are 4 different time units and 6 different pluralities. |
| int ids[4][6]; |
| }; |
| |
| // Message IDs for different time formats. |
| static const MessageIDs kTimeShortMessageIDs = { { |
| { |
| IDS_TIME_SECS_DEFAULT, IDS_TIME_SEC_SINGULAR, IDS_TIME_SECS_ZERO, |
| IDS_TIME_SECS_TWO, IDS_TIME_SECS_FEW, IDS_TIME_SECS_MANY |
| }, |
| { |
| IDS_TIME_MINS_DEFAULT, IDS_TIME_MIN_SINGULAR, IDS_TIME_MINS_ZERO, |
| IDS_TIME_MINS_TWO, IDS_TIME_MINS_FEW, IDS_TIME_MINS_MANY |
| }, |
| { |
| IDS_TIME_HOURS_DEFAULT, IDS_TIME_HOUR_SINGULAR, IDS_TIME_HOURS_ZERO, |
| IDS_TIME_HOURS_TWO, IDS_TIME_HOURS_FEW, IDS_TIME_HOURS_MANY |
| }, |
| { |
| IDS_TIME_DAYS_DEFAULT, IDS_TIME_DAY_SINGULAR, IDS_TIME_DAYS_ZERO, |
| IDS_TIME_DAYS_TWO, IDS_TIME_DAYS_FEW, IDS_TIME_DAYS_MANY |
| } |
| } }; |
| |
| static const MessageIDs kTimeRemainingMessageIDs = { { |
| { |
| IDS_TIME_REMAINING_SECS_DEFAULT, IDS_TIME_REMAINING_SEC_SINGULAR, |
| IDS_TIME_REMAINING_SECS_ZERO, IDS_TIME_REMAINING_SECS_TWO, |
| IDS_TIME_REMAINING_SECS_FEW, IDS_TIME_REMAINING_SECS_MANY |
| }, |
| { |
| IDS_TIME_REMAINING_MINS_DEFAULT, IDS_TIME_REMAINING_MIN_SINGULAR, |
| IDS_TIME_REMAINING_MINS_ZERO, IDS_TIME_REMAINING_MINS_TWO, |
| IDS_TIME_REMAINING_MINS_FEW, IDS_TIME_REMAINING_MINS_MANY |
| }, |
| { |
| IDS_TIME_REMAINING_HOURS_DEFAULT, IDS_TIME_REMAINING_HOUR_SINGULAR, |
| IDS_TIME_REMAINING_HOURS_ZERO, IDS_TIME_REMAINING_HOURS_TWO, |
| IDS_TIME_REMAINING_HOURS_FEW, IDS_TIME_REMAINING_HOURS_MANY |
| }, |
| { |
| IDS_TIME_REMAINING_DAYS_DEFAULT, IDS_TIME_REMAINING_DAY_SINGULAR, |
| IDS_TIME_REMAINING_DAYS_ZERO, IDS_TIME_REMAINING_DAYS_TWO, |
| IDS_TIME_REMAINING_DAYS_FEW, IDS_TIME_REMAINING_DAYS_MANY |
| } |
| } }; |
| |
| static const MessageIDs kTimeElapsedMessageIDs = { { |
| { |
| IDS_TIME_ELAPSED_SECS_DEFAULT, IDS_TIME_ELAPSED_SEC_SINGULAR, |
| IDS_TIME_ELAPSED_SECS_ZERO, IDS_TIME_ELAPSED_SECS_TWO, |
| IDS_TIME_ELAPSED_SECS_FEW, IDS_TIME_ELAPSED_SECS_MANY |
| }, |
| { |
| IDS_TIME_ELAPSED_MINS_DEFAULT, IDS_TIME_ELAPSED_MIN_SINGULAR, |
| IDS_TIME_ELAPSED_MINS_ZERO, IDS_TIME_ELAPSED_MINS_TWO, |
| IDS_TIME_ELAPSED_MINS_FEW, IDS_TIME_ELAPSED_MINS_MANY |
| }, |
| { |
| IDS_TIME_ELAPSED_HOURS_DEFAULT, IDS_TIME_ELAPSED_HOUR_SINGULAR, |
| IDS_TIME_ELAPSED_HOURS_ZERO, IDS_TIME_ELAPSED_HOURS_TWO, |
| IDS_TIME_ELAPSED_HOURS_FEW, IDS_TIME_ELAPSED_HOURS_MANY |
| }, |
| { |
| IDS_TIME_ELAPSED_DAYS_DEFAULT, IDS_TIME_ELAPSED_DAY_SINGULAR, |
| IDS_TIME_ELAPSED_DAYS_ZERO, IDS_TIME_ELAPSED_DAYS_TWO, |
| IDS_TIME_ELAPSED_DAYS_FEW, IDS_TIME_ELAPSED_DAYS_MANY |
| } |
| } }; |
| |
| // Different format types. |
| enum FormatType { |
| FORMAT_SHORT, |
| FORMAT_REMAINING, |
| FORMAT_ELAPSED, |
| }; |
| |
| } // namespace |
| |
| class TimeFormatter { |
| public: |
| const std::vector<icu::PluralFormat*>& formatter(FormatType format_type) { |
| switch (format_type) { |
| case FORMAT_SHORT: |
| return short_formatter_; |
| case FORMAT_REMAINING: |
| return time_left_formatter_; |
| case FORMAT_ELAPSED: |
| return time_elapsed_formatter_; |
| default: |
| NOTREACHED(); |
| return short_formatter_; |
| } |
| } |
| private: |
| static const MessageIDs& GetMessageIDs(FormatType format_type) { |
| switch (format_type) { |
| case FORMAT_SHORT: |
| return kTimeShortMessageIDs; |
| case FORMAT_REMAINING: |
| return kTimeRemainingMessageIDs; |
| case FORMAT_ELAPSED: |
| return kTimeElapsedMessageIDs; |
| default: |
| NOTREACHED(); |
| return kTimeShortMessageIDs; |
| } |
| } |
| |
| static const char* GetFallbackFormatSuffix(FormatType format_type) { |
| switch (format_type) { |
| case FORMAT_SHORT: |
| return kFallbackFormatSuffixShort; |
| case FORMAT_REMAINING: |
| return kFallbackFormatSuffixLeft; |
| case FORMAT_ELAPSED: |
| return kFallbackFormatSuffixAgo; |
| default: |
| NOTREACHED(); |
| return kFallbackFormatSuffixShort; |
| } |
| } |
| |
| TimeFormatter() { |
| BuildFormats(FORMAT_SHORT, &short_formatter_); |
| BuildFormats(FORMAT_REMAINING, &time_left_formatter_); |
| BuildFormats(FORMAT_ELAPSED, &time_elapsed_formatter_); |
| } |
| ~TimeFormatter() { |
| STLDeleteContainerPointers(short_formatter_.begin(), |
| short_formatter_.end()); |
| STLDeleteContainerPointers(time_left_formatter_.begin(), |
| time_left_formatter_.end()); |
| STLDeleteContainerPointers(time_elapsed_formatter_.begin(), |
| time_elapsed_formatter_.end()); |
| } |
| friend struct base::DefaultLazyInstanceTraits<TimeFormatter>; |
| |
| std::vector<icu::PluralFormat*> short_formatter_; |
| std::vector<icu::PluralFormat*> time_left_formatter_; |
| std::vector<icu::PluralFormat*> time_elapsed_formatter_; |
| static void BuildFormats(FormatType format_type, |
| std::vector<icu::PluralFormat*>* time_formats); |
| static icu::PluralFormat* createFallbackFormat( |
| const icu::PluralRules& rules, int index, FormatType format_type); |
| |
| DISALLOW_COPY_AND_ASSIGN(TimeFormatter); |
| }; |
| |
| static base::LazyInstance<TimeFormatter> g_time_formatter( |
| base::LINKER_INITIALIZED); |
| |
| void TimeFormatter::BuildFormats( |
| FormatType format_type, std::vector<icu::PluralFormat*>* time_formats) { |
| static const icu::UnicodeString kKeywords[] = { |
| UNICODE_STRING_SIMPLE("other"), UNICODE_STRING_SIMPLE("one"), |
| UNICODE_STRING_SIMPLE("zero"), UNICODE_STRING_SIMPLE("two"), |
| UNICODE_STRING_SIMPLE("few"), UNICODE_STRING_SIMPLE("many") |
| }; |
| UErrorCode err = U_ZERO_ERROR; |
| scoped_ptr<icu::PluralRules> rules( |
| icu::PluralRules::forLocale(icu::Locale::getDefault(), err)); |
| if (U_FAILURE(err)) { |
| err = U_ZERO_ERROR; |
| icu::UnicodeString fallback_rules("one: n is 1", -1, US_INV); |
| rules.reset(icu::PluralRules::createRules(fallback_rules, err)); |
| DCHECK(U_SUCCESS(err)); |
| } |
| |
| const MessageIDs& message_ids = GetMessageIDs(format_type); |
| |
| for (int i = 0; i < 4; ++i) { |
| icu::UnicodeString pattern; |
| for (size_t j = 0; j < arraysize(kKeywords); ++j) { |
| int msg_id = message_ids.ids[i][j]; |
| std::string sub_pattern = l10n_util::GetStringUTF8(msg_id); |
| // NA means this keyword is not used in the current locale. |
| // Even if a translator translated for this keyword, we do not |
| // use it unless it's 'other' (j=0) or it's defined in the rules |
| // for the current locale. Special-casing of 'other' will be removed |
| // once ICU's isKeyword is fixed to return true for isKeyword('other'). |
| if (sub_pattern.compare("NA") != 0 && |
| (j == 0 || rules->isKeyword(kKeywords[j]))) { |
| pattern += kKeywords[j]; |
| pattern += UNICODE_STRING_SIMPLE("{"); |
| pattern += icu::UnicodeString(sub_pattern.c_str(), "UTF-8"); |
| pattern += UNICODE_STRING_SIMPLE("}"); |
| } |
| } |
| icu::PluralFormat* format = new icu::PluralFormat(*rules, pattern, err); |
| if (U_SUCCESS(err)) { |
| time_formats->push_back(format); |
| } else { |
| delete format; |
| time_formats->push_back(createFallbackFormat(*rules, i, format_type)); |
| // Reset it so that next ICU call can proceed. |
| err = U_ZERO_ERROR; |
| } |
| } |
| } |
| |
| // Create a hard-coded fallback plural format. This will never be called |
| // unless translators make a mistake. |
| icu::PluralFormat* TimeFormatter::createFallbackFormat( |
| const icu::PluralRules& rules, int index, FormatType format_type) { |
| static const icu::UnicodeString kUnits[4][2] = { |
| { UNICODE_STRING_SIMPLE("sec"), UNICODE_STRING_SIMPLE("secs") }, |
| { UNICODE_STRING_SIMPLE("min"), UNICODE_STRING_SIMPLE("mins") }, |
| { UNICODE_STRING_SIMPLE("hour"), UNICODE_STRING_SIMPLE("hours") }, |
| { UNICODE_STRING_SIMPLE("day"), UNICODE_STRING_SIMPLE("days") } |
| }; |
| icu::UnicodeString suffix(GetFallbackFormatSuffix(format_type), -1, US_INV); |
| icu::UnicodeString pattern; |
| if (rules.isKeyword(UNICODE_STRING_SIMPLE("one"))) { |
| pattern += UNICODE_STRING_SIMPLE("one{# ") + kUnits[index][0] + suffix; |
| } |
| pattern += UNICODE_STRING_SIMPLE(" other{# ") + kUnits[index][1] + suffix; |
| UErrorCode err = U_ZERO_ERROR; |
| icu::PluralFormat* format = new icu::PluralFormat(rules, pattern, err); |
| DCHECK(U_SUCCESS(err)); |
| return format; |
| } |
| |
| static string16 FormatTimeImpl(const TimeDelta& delta, FormatType format_type) { |
| if (delta.ToInternalValue() < 0) { |
| NOTREACHED() << "Negative duration"; |
| return string16(); |
| } |
| |
| int number; |
| |
| const std::vector<icu::PluralFormat*>& formatters = |
| g_time_formatter.Get().formatter(format_type); |
| |
| UErrorCode error = U_ZERO_ERROR; |
| icu::UnicodeString time_string; |
| // Less than a minute gets "X seconds left" |
| if (delta.ToInternalValue() < Time::kMicrosecondsPerMinute) { |
| number = static_cast<int>(delta.ToInternalValue() / |
| Time::kMicrosecondsPerSecond); |
| time_string = formatters[0]->format(number, error); |
| |
| // Less than 1 hour gets "X minutes left". |
| } else if (delta.ToInternalValue() < Time::kMicrosecondsPerHour) { |
| number = static_cast<int>(delta.ToInternalValue() / |
| Time::kMicrosecondsPerMinute); |
| time_string = formatters[1]->format(number, error); |
| |
| // Less than 1 day remaining gets "X hours left" |
| } else if (delta.ToInternalValue() < Time::kMicrosecondsPerDay) { |
| number = static_cast<int>(delta.ToInternalValue() / |
| Time::kMicrosecondsPerHour); |
| time_string = formatters[2]->format(number, error); |
| |
| // Anything bigger gets "X days left" |
| } else { |
| number = static_cast<int>(delta.ToInternalValue() / |
| Time::kMicrosecondsPerDay); |
| time_string = formatters[3]->format(number, error); |
| } |
| |
| // With the fallback added, this should never fail. |
| DCHECK(U_SUCCESS(error)); |
| int capacity = time_string.length() + 1; |
| string16 result; |
| time_string.extract(static_cast<UChar*>( |
| WriteInto(&result, capacity)), |
| capacity, error); |
| DCHECK(U_SUCCESS(error)); |
| return result; |
| } |
| |
| // static |
| string16 TimeFormat::TimeElapsed(const TimeDelta& delta) { |
| return FormatTimeImpl(delta, FORMAT_ELAPSED); |
| } |
| |
| // static |
| string16 TimeFormat::TimeRemaining(const TimeDelta& delta) { |
| return FormatTimeImpl(delta, FORMAT_REMAINING); |
| } |
| |
| // static |
| string16 TimeFormat::TimeRemainingShort(const TimeDelta& delta) { |
| return FormatTimeImpl(delta, FORMAT_SHORT); |
| } |
| |
| // static |
| string16 TimeFormat::RelativeDate( |
| const Time& time, |
| const Time* optional_midnight_today) { |
| Time midnight_today = optional_midnight_today ? *optional_midnight_today : |
| Time::Now().LocalMidnight(); |
| |
| // Filter out "today" and "yesterday" |
| if (time >= midnight_today) |
| return l10n_util::GetStringUTF16(IDS_PAST_TIME_TODAY); |
| else if (time >= midnight_today - |
| TimeDelta::FromMicroseconds(Time::kMicrosecondsPerDay)) |
| return l10n_util::GetStringUTF16(IDS_PAST_TIME_YESTERDAY); |
| |
| return string16(); |
| } |