// © 2019 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html // localeprioritylist.cpp // created: 2019jul11 Markus W. Scherer #include "unicode/utypes.h" #include "unicode/localpointer.h" #include "unicode/locid.h" #include "unicode/stringpiece.h" #include "unicode/uobject.h" #include "charstr.h" #include "cmemory.h" #include "localeprioritylist.h" #include "uarrsort.h" #include "uassert.h" #include "uhash.h" U_NAMESPACE_BEGIN namespace { int32_t hashLocale(const UHashTok token) { const auto* locale = static_cast(token.pointer); return locale->hashCode(); } UBool compareLocales(const UHashTok t1, const UHashTok t2) { const auto* l1 = static_cast(t1.pointer); const auto* l2 = static_cast(t2.pointer); return *l1 == *l2; } constexpr int32_t WEIGHT_ONE = 1000; struct LocaleAndWeight { Locale *locale; int32_t weight; // 0..1000 = 0.0..1.0 int32_t index; // force stable sort int32_t compare(const LocaleAndWeight &other) const { int32_t diff = other.weight - weight; // descending: other-this if (diff != 0) { return diff; } return index - other.index; } }; int32_t U_CALLCONV compareLocaleAndWeight(const void * /*context*/, const void *left, const void *right) { return static_cast(left)-> compare(*static_cast(right)); } const char *skipSpaces(const char *p, const char *limit) { while (p < limit && *p == ' ') { ++p; } return p; } int32_t findTagLength(const char *p, const char *limit) { // Look for accept-language delimiters. // Leave other validation up to the Locale constructor. const char *q; for (q = p; q < limit; ++q) { char c = *q; if (c == ' ' || c == ',' || c == ';') { break; } } return static_cast(q - p); } /** * Parses and returns a qvalue weight in millis. * Advances p to after the parsed substring. * Returns a negative value if parsing fails. */ int32_t parseWeight(const char *&p, const char *limit) { p = skipSpaces(p, limit); char c; if (p == limit || ((c = *p) != '0' && c != '1')) { return -1; } int32_t weight = (c - '0') * 1000; if (++p == limit || *p != '.') { return weight; } int32_t multiplier = 100; while (++p != limit && '0' <= (c = *p) && c <= '9') { c -= '0'; if (multiplier > 0) { weight += c * multiplier; multiplier /= 10; } else if (multiplier == 0) { // round up if (c >= 5) { ++weight; } multiplier = -1; } // else ignore further fraction digits } return weight <= WEIGHT_ONE ? weight : -1; // bad if > 1.0 } } // namespace /** * Nothing but a wrapper over a MaybeStackArray of LocaleAndWeight. * * This wrapper exists (and is not in an anonymous namespace) * so that we can forward-declare it in the header file and * don't have to expose the MaybeStackArray specialization and * the LocaleAndWeight to code (like the test) that #includes localeprioritylist.h. * Also, otherwise we would have to do a platform-specific * template export declaration of some kind for the MaybeStackArray specialization * to be properly exported from the common DLL. */ struct LocaleAndWeightArray : public UMemory { MaybeStackArray array; }; LocalePriorityList::LocalePriorityList(StringPiece s, UErrorCode &errorCode) { if (U_FAILURE(errorCode)) { return; } list = new LocaleAndWeightArray(); if (list == nullptr) { errorCode = U_MEMORY_ALLOCATION_ERROR; return; } const char *p = s.data(); const char *limit = p + s.length(); while ((p = skipSpaces(p, limit)) != limit) { if (*p == ',') { // empty range field ++p; continue; } int32_t tagLength = findTagLength(p, limit); if (tagLength == 0) { errorCode = U_ILLEGAL_ARGUMENT_ERROR; return; } CharString tag(p, tagLength, errorCode); if (U_FAILURE(errorCode)) { return; } Locale locale = Locale(tag.data()); if (locale.isBogus()) { errorCode = U_ILLEGAL_ARGUMENT_ERROR; return; } int32_t weight = WEIGHT_ONE; if ((p = skipSpaces(p + tagLength, limit)) != limit && *p == ';') { if ((p = skipSpaces(p + 1, limit)) == limit || *p != 'q' || (p = skipSpaces(p + 1, limit)) == limit || *p != '=' || (++p, (weight = parseWeight(p, limit)) < 0)) { errorCode = U_ILLEGAL_ARGUMENT_ERROR; return; } p = skipSpaces(p, limit); } if (p != limit && *p != ',') { // trailing junk errorCode = U_ILLEGAL_ARGUMENT_ERROR; return; } add(locale, weight, errorCode); if (p == limit) { break; } ++p; } sort(errorCode); } LocalePriorityList::~LocalePriorityList() { if (list != nullptr) { for (int32_t i = 0; i < listLength; ++i) { delete list->array[i].locale; } delete list; } uhash_close(map); } const Locale *LocalePriorityList::localeAt(int32_t i) const { return list->array[i].locale; } Locale *LocalePriorityList::orphanLocaleAt(int32_t i) { if (list == nullptr) { return nullptr; } LocaleAndWeight &lw = list->array[i]; Locale *l = lw.locale; lw.locale = nullptr; return l; } bool LocalePriorityList::add(const Locale &locale, int32_t weight, UErrorCode &errorCode) { if (U_FAILURE(errorCode)) { return false; } if (map == nullptr) { if (weight <= 0) { return true; } // do not add q=0 map = uhash_open(hashLocale, compareLocales, uhash_compareLong, &errorCode); if (U_FAILURE(errorCode)) { return false; } } LocalPointer clone; UBool found = false; int32_t index = uhash_getiAndFound(map, &locale, &found); if (found) { // Duplicate: Remove the old item and append it anew. LocaleAndWeight &lw = list->array[index]; clone.adoptInstead(lw.locale); lw.locale = nullptr; lw.weight = 0; ++numRemoved; } if (weight <= 0) { // do not add q=0 if (found) { // Not strictly necessary but cleaner. uhash_removei(map, &locale); } return true; } if (clone.isNull()) { clone.adoptInstead(locale.clone()); if (clone.isNull() || (clone->isBogus() && !locale.isBogus())) { errorCode = U_MEMORY_ALLOCATION_ERROR; return false; } } if (listLength == list->array.getCapacity()) { int32_t newCapacity = listLength < 50 ? 100 : 4 * listLength; if (list->array.resize(newCapacity, listLength) == nullptr) { errorCode = U_MEMORY_ALLOCATION_ERROR; return false; } } uhash_putiAllowZero(map, clone.getAlias(), listLength, &errorCode); if (U_FAILURE(errorCode)) { return false; } LocaleAndWeight &lw = list->array[listLength]; lw.locale = clone.orphan(); lw.weight = weight; lw.index = listLength++; if (weight < WEIGHT_ONE) { hasWeights = true; } U_ASSERT(uhash_count(map) == getLength()); return true; } void LocalePriorityList::sort(UErrorCode &errorCode) { // Sort by descending weights if there is a mix of weights. // The comparator forces a stable sort via the item index. if (U_FAILURE(errorCode) || getLength() <= 1 || !hasWeights) { return; } uprv_sortArray(list->array.getAlias(), listLength, sizeof(LocaleAndWeight), compareLocaleAndWeight, nullptr, false, &errorCode); } U_NAMESPACE_END