Home | History | Annotate | Download | only in i18n
      1 /*
      2 *******************************************************************************
      3 * Copyright (C) 2007-2010, International Business Machines Corporation and    *
      4 * others. All Rights Reserved.                                                *
      5 *******************************************************************************
      6 */
      7 
      8 #include "unicode/utypes.h"
      9 
     10 #if !UCONFIG_NO_FORMATTING
     11 
     12 #include "zonemeta.h"
     13 
     14 #include "unicode/timezone.h"
     15 #include "unicode/ustring.h"
     16 #include "unicode/putil.h"
     17 
     18 #include "umutex.h"
     19 #include "uvector.h"
     20 #include "cmemory.h"
     21 #include "gregoimp.h"
     22 #include "cstring.h"
     23 #include "ucln_in.h"
     24 #include "uassert.h"
     25 
     26 static UMTX gZoneMetaLock = NULL;
     27 
     28 // Metazone mapping table
     29 static UHashtable *gOlsonToMeta = NULL;
     30 static UBool gOlsonToMetaInitialized = FALSE;
     31 
     32 // Country info vectors
     33 static U_NAMESPACE_QUALIFIER UVector *gSingleZoneCountries = NULL;
     34 static U_NAMESPACE_QUALIFIER UVector *gMultiZonesCountries = NULL;
     35 static UBool gCountryInfoVectorsInitialized = FALSE;
     36 
     37 U_CDECL_BEGIN
     38 
     39 
     40 /**
     41  * Cleanup callback func
     42  */
     43 static UBool U_CALLCONV zoneMeta_cleanup(void)
     44 {
     45      umtx_destroy(&gZoneMetaLock);
     46 
     47     if (gOlsonToMeta != NULL) {
     48         uhash_close(gOlsonToMeta);
     49         gOlsonToMeta = NULL;
     50     }
     51     gOlsonToMetaInitialized = FALSE;
     52 
     53     delete gSingleZoneCountries;
     54     delete gMultiZonesCountries;
     55     gCountryInfoVectorsInitialized = FALSE;
     56 
     57     return TRUE;
     58 }
     59 
     60 /**
     61  * Deleter for UChar* string
     62  */
     63 static void U_CALLCONV
     64 deleteUCharString(void *obj) {
     65     UChar *entry = (UChar*)obj;
     66     uprv_free(entry);
     67 }
     68 
     69 /**
     70  * Deleter for UVector
     71  */
     72 static void U_CALLCONV
     73 deleteUVector(void *obj) {
     74    delete (U_NAMESPACE_QUALIFIER UVector*) obj;
     75 }
     76 
     77 /**
     78  * Deleter for OlsonToMetaMappingEntry
     79  */
     80 static void U_CALLCONV
     81 deleteOlsonToMetaMappingEntry(void *obj) {
     82     U_NAMESPACE_QUALIFIER OlsonToMetaMappingEntry *entry = (U_NAMESPACE_QUALIFIER OlsonToMetaMappingEntry*)obj;
     83     uprv_free(entry);
     84 }
     85 
     86 U_CDECL_END
     87 
     88 U_NAMESPACE_BEGIN
     89 
     90 #define ZID_KEY_MAX 128
     91 
     92 static const char gMetaZones[]          = "metaZones";
     93 static const char gMetazoneInfo[]       = "metazoneInfo";
     94 static const char gMapTimezonesTag[]    = "mapTimezones";
     95 
     96 static const char gTimeZoneTypes[]      = "timezoneTypes";
     97 static const char gTypeAliasTag[]       = "typeAlias";
     98 static const char gTypeMapTag[]         = "typeMap";
     99 static const char gTimezoneTag[]        = "timezone";
    100 
    101 static const char gWorldTag[]           = "001";
    102 
    103 static const UChar gWorld[] = {0x30, 0x30, 0x31, 0x00}; // "001"
    104 
    105 static const UChar gDefaultFrom[] = {0x31, 0x39, 0x37, 0x30, 0x2D, 0x30, 0x31, 0x2D, 0x30, 0x31,
    106                                      0x20, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x00}; // "1970-01-01 00:00"
    107 static const UChar gDefaultTo[]   = {0x39, 0x39, 0x39, 0x39, 0x2D, 0x31, 0x32, 0x2D, 0x33, 0x31,
    108                                      0x20, 0x32, 0x33, 0x3A, 0x35, 0x39, 0x00}; // "9999-12-31 23:59"
    109 
    110 #define ASCII_DIGIT(c) (((c)>=0x30 && (c)<=0x39) ? (c)-0x30 : -1)
    111 
    112 /*
    113  * Convert a date string used by metazone mappings to UDate.
    114  * The format used by CLDR metazone mapping is "yyyy-MM-dd HH:mm".
    115  */
    116 static UDate
    117 parseDate (const UChar *text, UErrorCode &status) {
    118     if (U_FAILURE(status)) {
    119         return 0;
    120     }
    121     int32_t len = u_strlen(text);
    122     if (len != 16 && len != 10) {
    123         // It must be yyyy-MM-dd HH:mm (length 16) or yyyy-MM-dd (length 10)
    124         status = U_INVALID_FORMAT_ERROR;
    125         return 0;
    126     }
    127 
    128     int32_t year = 0, month = 0, day = 0, hour = 0, min = 0, n;
    129     int32_t idx;
    130 
    131     // "yyyy" (0 - 3)
    132     for (idx = 0; idx <= 3 && U_SUCCESS(status); idx++) {
    133         n = ASCII_DIGIT((int32_t)text[idx]);
    134         if (n >= 0) {
    135             year = 10*year + n;
    136         } else {
    137             status = U_INVALID_FORMAT_ERROR;
    138         }
    139     }
    140     // "MM" (5 - 6)
    141     for (idx = 5; idx <= 6 && U_SUCCESS(status); idx++) {
    142         n = ASCII_DIGIT((int32_t)text[idx]);
    143         if (n >= 0) {
    144             month = 10*month + n;
    145         } else {
    146             status = U_INVALID_FORMAT_ERROR;
    147         }
    148     }
    149     // "dd" (8 - 9)
    150     for (idx = 8; idx <= 9 && U_SUCCESS(status); idx++) {
    151         n = ASCII_DIGIT((int32_t)text[idx]);
    152         if (n >= 0) {
    153             day = 10*day + n;
    154         } else {
    155             status = U_INVALID_FORMAT_ERROR;
    156         }
    157     }
    158     if (len == 16) {
    159         // "HH" (11 - 12)
    160         for (idx = 11; idx <= 12 && U_SUCCESS(status); idx++) {
    161             n = ASCII_DIGIT((int32_t)text[idx]);
    162             if (n >= 0) {
    163                 hour = 10*hour + n;
    164             } else {
    165                 status = U_INVALID_FORMAT_ERROR;
    166             }
    167         }
    168         // "mm" (14 - 15)
    169         for (idx = 14; idx <= 15 && U_SUCCESS(status); idx++) {
    170             n = ASCII_DIGIT((int32_t)text[idx]);
    171             if (n >= 0) {
    172                 min = 10*min + n;
    173             } else {
    174                 status = U_INVALID_FORMAT_ERROR;
    175             }
    176         }
    177     }
    178 
    179     if (U_SUCCESS(status)) {
    180         UDate date = Grego::fieldsToDay(year, month - 1, day) * U_MILLIS_PER_DAY
    181             + hour * U_MILLIS_PER_HOUR + min * U_MILLIS_PER_MINUTE;
    182         return date;
    183     }
    184     return 0;
    185 }
    186 
    187 UnicodeString& U_EXPORT2
    188 ZoneMeta::getCanonicalSystemID(const UnicodeString &tzid, UnicodeString &systemID, UErrorCode& status) {
    189     int32_t len = tzid.length();
    190     if ( len >= ZID_KEY_MAX ) {
    191         status = U_ILLEGAL_ARGUMENT_ERROR;
    192         systemID.remove();
    193         return systemID;
    194     }
    195 
    196     char id[ZID_KEY_MAX];
    197     const UChar* idChars = tzid.getBuffer();
    198 
    199     u_UCharsToChars(idChars,id,len);
    200     id[len] = (char) 0; // Make sure it is null terminated.
    201 
    202     // replace '/' with ':'
    203     char *p = id;
    204     while (*p++) {
    205         if (*p == '/') {
    206             *p = ':';
    207         }
    208     }
    209 
    210 
    211     UErrorCode tmpStatus = U_ZERO_ERROR;
    212     UResourceBundle *top = ures_openDirect(NULL, gTimeZoneTypes, &tmpStatus);
    213     UResourceBundle *rb = ures_getByKey(top, gTypeMapTag, NULL, &tmpStatus);
    214     ures_getByKey(rb, gTimezoneTag, rb, &tmpStatus);
    215     ures_getByKey(rb, id, rb, &tmpStatus);
    216     if (U_SUCCESS(tmpStatus)) {
    217         // direct map found
    218         systemID.setTo(tzid);
    219         ures_close(rb);
    220         ures_close(top);
    221         return systemID;
    222     }
    223 
    224     // If a map element not found, then look for an alias
    225     tmpStatus = U_ZERO_ERROR;
    226     ures_getByKey(top, gTypeAliasTag, rb, &tmpStatus);
    227     ures_getByKey(rb, gTimezoneTag, rb, &tmpStatus);
    228     const UChar *alias = ures_getStringByKey(rb,id,NULL,&tmpStatus);
    229     if (U_SUCCESS(tmpStatus)) {
    230         // alias found
    231         ures_close(rb);
    232         ures_close(top);
    233         systemID.setTo(alias);
    234         return systemID;
    235     }
    236 
    237     // Dereference the input ID using the tz data
    238     const UChar *derefer = TimeZone::dereferOlsonLink(tzid);
    239     if (derefer == NULL) {
    240         systemID.remove();
    241         status = U_ILLEGAL_ARGUMENT_ERROR;
    242     } else {
    243 
    244         len = u_strlen(derefer);
    245         u_UCharsToChars(derefer,id,len);
    246         id[len] = (char) 0; // Make sure it is null terminated.
    247 
    248         // replace '/' with ':'
    249         char *p = id;
    250         while (*p++) {
    251             if (*p == '/') {
    252                 *p = ':';
    253             }
    254         }
    255 
    256         // If a dereference turned something up then look for an alias.
    257         // rb still points to the alias table, so we don't have to go looking
    258         // for it.
    259         tmpStatus = U_ZERO_ERROR;
    260         const UChar *alias = ures_getStringByKey(rb,id,NULL,&tmpStatus);
    261         if (U_SUCCESS(tmpStatus)) {
    262             // alias found
    263             systemID.setTo(alias);
    264         } else {
    265             systemID.setTo(derefer);
    266         }
    267     }
    268 
    269      ures_close(rb);
    270      ures_close(top);
    271      return systemID;
    272 }
    273 
    274 UnicodeString& U_EXPORT2
    275 ZoneMeta::getCanonicalCountry(const UnicodeString &tzid, UnicodeString &canonicalCountry) {
    276     const UChar *region = TimeZone::getRegion(tzid);
    277     if (u_strcmp(gWorld, region) != 0) {
    278         canonicalCountry.setTo(region, -1);
    279     } else {
    280         canonicalCountry.remove();
    281     }
    282     return canonicalCountry;
    283 }
    284 
    285 UnicodeString& U_EXPORT2
    286 ZoneMeta::getSingleCountry(const UnicodeString &tzid, UnicodeString &country) {
    287     // Get canonical country for the zone
    288     const UChar *region = TimeZone::getRegion(tzid);
    289     if (u_strcmp(gWorld, region) == 0) {
    290         // special case - "001"
    291         country.remove();
    292         return country;
    293     }
    294 
    295     // Checking the cached results
    296     UErrorCode status = U_ZERO_ERROR;
    297     UBool initialized;
    298     UMTX_CHECK(&gZoneMetaLock, gCountryInfoVectorsInitialized, initialized);
    299     if (!initialized) {
    300         // Create empty vectors
    301         umtx_lock(&gZoneMetaLock);
    302         {
    303             if (!gCountryInfoVectorsInitialized) {
    304                 // No deleters for these UVectors, it's a reference to a resource bundle string.
    305                 gSingleZoneCountries = new UVector(NULL, uhash_compareUChars, status);
    306                 if (gSingleZoneCountries == NULL) {
    307                     status = U_MEMORY_ALLOCATION_ERROR;
    308                 }
    309                 gMultiZonesCountries = new UVector(NULL, uhash_compareUChars, status);
    310                 if (gMultiZonesCountries == NULL) {
    311                     status = U_MEMORY_ALLOCATION_ERROR;
    312                 }
    313 
    314                 if (U_SUCCESS(status)) {
    315                     gCountryInfoVectorsInitialized = TRUE;
    316                 } else {
    317                     delete gSingleZoneCountries;
    318                     delete gMultiZonesCountries;
    319                 }
    320             }
    321         }
    322         umtx_unlock(&gZoneMetaLock);
    323 
    324         if (U_FAILURE(status)) {
    325             country.remove();
    326             return country;
    327         }
    328     }
    329 
    330     // Check if it was already cached
    331     UBool cached = FALSE;
    332     UBool multiZones = FALSE;
    333     umtx_lock(&gZoneMetaLock);
    334     {
    335         multiZones = cached = gMultiZonesCountries->contains((void*)region);
    336         if (!multiZones) {
    337             cached = gSingleZoneCountries->contains((void*)region);
    338         }
    339     }
    340     umtx_unlock(&gZoneMetaLock);
    341 
    342     if (!cached) {
    343         // We need to go through all zones associated with the region.
    344         // This is relatively heavy operation.
    345 
    346         U_ASSERT(u_strlen(region) == 2);
    347 
    348         char buf[] = {0, 0, 0};
    349         u_UCharsToChars(region, buf, 2);
    350 
    351         StringEnumeration *ids = TimeZone::createEnumeration(buf);
    352         int32_t idsLen = ids->count(status);
    353         if (U_SUCCESS(status) && idsLen > 1) {
    354             // multiple zones are available for the region
    355             UnicodeString canonical, tmp;
    356             const UnicodeString *id = ids->snext(status);
    357             getCanonicalSystemID(*id, canonical, status);
    358             if (U_SUCCESS(status)) {
    359                 // check if there are any other canonical zone in the group
    360                 while ((id = ids->snext(status))!=NULL) {
    361                     getCanonicalSystemID(*id, tmp, status);
    362                     if (U_FAILURE(status)) {
    363                         break;
    364                     }
    365                     if (canonical != tmp) {
    366                         // another canonical zone was found
    367                         multiZones = TRUE;
    368                         break;
    369                     }
    370                 }
    371             }
    372         }
    373         if (U_FAILURE(status)) {
    374             // no single country by default for any error cases
    375             multiZones = TRUE;
    376         }
    377         delete ids;
    378 
    379         // Cache the result
    380         umtx_lock(&gZoneMetaLock);
    381         {
    382             UErrorCode ec = U_ZERO_ERROR;
    383             if (multiZones) {
    384                 if (!gMultiZonesCountries->contains((void*)region)) {
    385                     gMultiZonesCountries->addElement((void*)region, ec);
    386                 }
    387             } else {
    388                 if (!gSingleZoneCountries->contains((void*)region)) {
    389                     gSingleZoneCountries->addElement((void*)region, ec);
    390                 }
    391             }
    392         }
    393         umtx_unlock(&gZoneMetaLock);
    394     }
    395 
    396     if (multiZones) {
    397         country.remove();
    398     } else {
    399         country.setTo(region, -1);
    400     }
    401     return country;
    402 }
    403 
    404 UnicodeString& U_EXPORT2
    405 ZoneMeta::getMetazoneID(const UnicodeString &tzid, UDate date, UnicodeString &result) {
    406     UBool isSet = FALSE;
    407     const UVector *mappings = getMetazoneMappings(tzid);
    408     if (mappings != NULL) {
    409         for (int32_t i = 0; i < mappings->size(); i++) {
    410             OlsonToMetaMappingEntry *mzm = (OlsonToMetaMappingEntry*)mappings->elementAt(i);
    411             if (mzm->from <= date && mzm->to > date) {
    412                 result.setTo(mzm->mzid, -1);
    413                 isSet = TRUE;
    414                 break;
    415             }
    416         }
    417     }
    418     if (!isSet) {
    419         result.remove();
    420     }
    421     return result;
    422 }
    423 
    424 const UVector* U_EXPORT2
    425 ZoneMeta::getMetazoneMappings(const UnicodeString &tzid) {
    426     UErrorCode status = U_ZERO_ERROR;
    427     UChar tzidUChars[ZID_KEY_MAX];
    428     tzid.extract(tzidUChars, ZID_KEY_MAX, status);
    429     if (U_FAILURE(status) || status == U_STRING_NOT_TERMINATED_WARNING) {
    430         return NULL;
    431     }
    432 
    433     UBool initialized;
    434     UMTX_CHECK(&gZoneMetaLock, gOlsonToMetaInitialized, initialized);
    435     if (!initialized) {
    436         UHashtable *tmpOlsonToMeta = uhash_open(uhash_hashUChars, uhash_compareUChars, NULL, &status);
    437         if (U_FAILURE(status)) {
    438             return NULL;
    439         }
    440         uhash_setKeyDeleter(tmpOlsonToMeta, deleteUCharString);
    441         uhash_setValueDeleter(tmpOlsonToMeta, deleteUVector);
    442 
    443         umtx_lock(&gZoneMetaLock);
    444         {
    445             if (!gOlsonToMetaInitialized) {
    446                 gOlsonToMeta = tmpOlsonToMeta;
    447                 tmpOlsonToMeta = NULL;
    448                 gOlsonToMetaInitialized = TRUE;
    449             }
    450         }
    451         umtx_unlock(&gZoneMetaLock);
    452 
    453         // OK to call the following multiple times with the same function
    454         ucln_i18n_registerCleanup(UCLN_I18N_ZONEMETA, zoneMeta_cleanup);
    455         if (tmpOlsonToMeta != NULL) {
    456             uhash_close(tmpOlsonToMeta);
    457         }
    458     }
    459 
    460     // get the mapping from cache
    461     const UVector *result = NULL;
    462 
    463     umtx_lock(&gZoneMetaLock);
    464     {
    465         result = (UVector*) uhash_get(gOlsonToMeta, tzidUChars);
    466     }
    467     umtx_unlock(&gZoneMetaLock);
    468 
    469     if (result != NULL) {
    470         return result;
    471     }
    472 
    473     // miss the cache - create new one
    474     UVector *tmpResult = createMetazoneMappings(tzid);
    475     if (tmpResult == NULL) {
    476         // not available
    477         return NULL;
    478     }
    479 
    480     // put the new one into the cache
    481     umtx_lock(&gZoneMetaLock);
    482     {
    483         // make sure it's already created
    484         result = (UVector*) uhash_get(gOlsonToMeta, tzidUChars);
    485         if (result == NULL) {
    486             // add the one just created
    487             int32_t tzidLen = tzid.length() + 1;
    488             UChar *key = (UChar*)uprv_malloc(tzidLen * sizeof(UChar));
    489             if (key == NULL) {
    490                 // memory allocation error..  just return NULL
    491                 result = NULL;
    492                 delete tmpResult;
    493             } else {
    494                 tzid.extract(key, tzidLen, status);
    495                 uhash_put(gOlsonToMeta, key, tmpResult, &status);
    496                 if (U_FAILURE(status)) {
    497                     // delete the mapping
    498                     result = NULL;
    499                     delete tmpResult;
    500                 } else {
    501                     result = tmpResult;
    502                 }
    503             }
    504         } else {
    505             // another thread already put the one
    506             delete tmpResult;
    507         }
    508     }
    509     umtx_unlock(&gZoneMetaLock);
    510 
    511     return result;
    512 }
    513 
    514 UVector*
    515 ZoneMeta::createMetazoneMappings(const UnicodeString &tzid) {
    516     UVector *mzMappings = NULL;
    517     UErrorCode status = U_ZERO_ERROR;
    518 
    519     UnicodeString canonicalID;
    520     UResourceBundle *rb = ures_openDirect(NULL, gMetaZones, &status);
    521     ures_getByKey(rb, gMetazoneInfo, rb, &status);
    522     TimeZone::getCanonicalID(tzid, canonicalID, status);
    523 
    524     if (U_SUCCESS(status)) {
    525         char tzKey[ZID_KEY_MAX];
    526         canonicalID.extract(0, canonicalID.length(), tzKey, sizeof(tzKey), US_INV);
    527 
    528         // tzid keys are using ':' as separators
    529         char *p = tzKey;
    530         while (*p) {
    531             if (*p == '/') {
    532                 *p = ':';
    533             }
    534             p++;
    535         }
    536 
    537         ures_getByKey(rb, tzKey, rb, &status);
    538 
    539         if (U_SUCCESS(status)) {
    540             UResourceBundle *mz = NULL;
    541             while (ures_hasNext(rb)) {
    542                 mz = ures_getNextResource(rb, mz, &status);
    543 
    544                 const UChar *mz_name = ures_getStringByIndex(mz, 0, NULL, &status);
    545                 const UChar *mz_from = gDefaultFrom;
    546                 const UChar *mz_to = gDefaultTo;
    547 
    548                 if (ures_getSize(mz) == 3) {
    549                     mz_from = ures_getStringByIndex(mz, 1, NULL, &status);
    550                     mz_to   = ures_getStringByIndex(mz, 2, NULL, &status);
    551                 }
    552 
    553                 if(U_FAILURE(status)){
    554                     status = U_ZERO_ERROR;
    555                     continue;
    556                 }
    557                 // We do not want to use SimpleDateformat to parse boundary dates,
    558                 // because this code could be triggered by the initialization code
    559                 // used by SimpleDateFormat.
    560                 UDate from = parseDate(mz_from, status);
    561                 UDate to = parseDate(mz_to, status);
    562                 if (U_FAILURE(status)) {
    563                     status = U_ZERO_ERROR;
    564                     continue;
    565                 }
    566 
    567                 OlsonToMetaMappingEntry *entry = (OlsonToMetaMappingEntry*)uprv_malloc(sizeof(OlsonToMetaMappingEntry));
    568                 if (entry == NULL) {
    569                     status = U_MEMORY_ALLOCATION_ERROR;
    570                     break;
    571                 }
    572                 entry->mzid = mz_name;
    573                 entry->from = from;
    574                 entry->to = to;
    575 
    576                 if (mzMappings == NULL) {
    577                     mzMappings = new UVector(deleteOlsonToMetaMappingEntry, NULL, status);
    578                     if (U_FAILURE(status)) {
    579                         delete mzMappings;
    580                         deleteOlsonToMetaMappingEntry(entry);
    581                         uprv_free(entry);
    582                         break;
    583                     }
    584                 }
    585 
    586                 mzMappings->addElement(entry, status);
    587                 if (U_FAILURE(status)) {
    588                     break;
    589                 }
    590             }
    591             ures_close(mz);
    592             if (U_FAILURE(status)) {
    593                 if (mzMappings != NULL) {
    594                     delete mzMappings;
    595                     mzMappings = NULL;
    596                 }
    597             }
    598         }
    599     }
    600     ures_close(rb);
    601     return mzMappings;
    602 }
    603 
    604 UnicodeString& U_EXPORT2
    605 ZoneMeta::getZoneIdByMetazone(const UnicodeString &mzid, const UnicodeString &region, UnicodeString &result) {
    606     UErrorCode status = U_ZERO_ERROR;
    607     const UChar *tzid = NULL;
    608     int32_t tzidLen = 0;
    609     char keyBuf[ZID_KEY_MAX + 1];
    610     int32_t keyLen = 0;
    611 
    612     if (mzid.length() >= ZID_KEY_MAX) {
    613         result.remove();
    614         return result;
    615     }
    616 
    617     keyLen = mzid.extract(0, mzid.length(), keyBuf, ZID_KEY_MAX, US_INV);
    618 
    619     UResourceBundle *rb = ures_openDirect(NULL, gMetaZones, &status);
    620     ures_getByKey(rb, gMapTimezonesTag, rb, &status);
    621     ures_getByKey(rb, keyBuf, rb, &status);
    622 
    623     if (U_SUCCESS(status)) {
    624         // check region mapping
    625         if (region.length() == 2 || region.length() == 3) {
    626             region.extract(0, region.length(), keyBuf, ZID_KEY_MAX, US_INV);
    627             tzid = ures_getStringByKey(rb, keyBuf, &tzidLen, &status);
    628             if (status == U_MISSING_RESOURCE_ERROR) {
    629                 status = U_ZERO_ERROR;
    630             }
    631         }
    632         if (U_SUCCESS(status) && tzid == NULL) {
    633             // try "001"
    634             tzid = ures_getStringByKey(rb, gWorldTag, &tzidLen, &status);
    635         }
    636     }
    637     ures_close(rb);
    638 
    639     if (tzid == NULL) {
    640         result.remove();
    641     } else {
    642         result.setTo(tzid, tzidLen);
    643     }
    644 
    645     return result;
    646 }
    647 
    648 U_NAMESPACE_END
    649 
    650 #endif /* #if !UCONFIG_NO_FORMATTING */
    651