2309 lines
74 KiB
C++
2309 lines
74 KiB
C++
// © 2016 and later: Unicode, Inc. and others.
|
|
// License & terms of use: http://www.unicode.org/copyright.html
|
|
/*
|
|
*******************************************************************************
|
|
* Copyright (C) 2011-2016, International Business Machines Corporation and
|
|
* others. All Rights Reserved.
|
|
*******************************************************************************
|
|
*
|
|
* File TZNAMES_IMPL.CPP
|
|
*
|
|
*******************************************************************************
|
|
*/
|
|
|
|
#include "unicode/utypes.h"
|
|
|
|
#if !UCONFIG_NO_FORMATTING
|
|
|
|
#include "unicode/strenum.h"
|
|
#include "unicode/ustring.h"
|
|
#include "unicode/timezone.h"
|
|
#include "unicode/utf16.h"
|
|
|
|
#include "tznames_impl.h"
|
|
#include "bytesinkutil.h"
|
|
#include "charstr.h"
|
|
#include "cmemory.h"
|
|
#include "cstring.h"
|
|
#include "uassert.h"
|
|
#include "mutex.h"
|
|
#include "resource.h"
|
|
#include "ulocimp.h"
|
|
#include "uresimp.h"
|
|
#include "ureslocs.h"
|
|
#include "zonemeta.h"
|
|
#include "ucln_in.h"
|
|
#include "uvector.h"
|
|
#include "olsontz.h"
|
|
|
|
U_NAMESPACE_BEGIN
|
|
|
|
#define ZID_KEY_MAX 128
|
|
#define MZ_PREFIX_LEN 5
|
|
|
|
static const char gZoneStrings[] = "zoneStrings";
|
|
static const char gMZPrefix[] = "meta:";
|
|
|
|
static const char EMPTY[] = "<empty>"; // place holder for empty ZNames
|
|
static const char DUMMY_LOADER[] = "<dummy>"; // place holder for dummy ZNamesLoader
|
|
static const UChar NO_NAME[] = { 0 }; // for empty no-fallback time zone names
|
|
|
|
// stuff for TZDBTimeZoneNames
|
|
static const char* TZDBNAMES_KEYS[] = {"ss", "sd"};
|
|
static const int32_t TZDBNAMES_KEYS_SIZE = UPRV_LENGTHOF(TZDBNAMES_KEYS);
|
|
|
|
static UMutex gDataMutex;
|
|
|
|
static UHashtable* gTZDBNamesMap = NULL;
|
|
static icu::UInitOnce gTZDBNamesMapInitOnce {};
|
|
|
|
static TextTrieMap* gTZDBNamesTrie = NULL;
|
|
static icu::UInitOnce gTZDBNamesTrieInitOnce {};
|
|
|
|
// The order in which strings are stored may be different than the order in the public enum.
|
|
enum UTimeZoneNameTypeIndex {
|
|
UTZNM_INDEX_UNKNOWN = -1,
|
|
UTZNM_INDEX_EXEMPLAR_LOCATION,
|
|
UTZNM_INDEX_LONG_GENERIC,
|
|
UTZNM_INDEX_LONG_STANDARD,
|
|
UTZNM_INDEX_LONG_DAYLIGHT,
|
|
UTZNM_INDEX_SHORT_GENERIC,
|
|
UTZNM_INDEX_SHORT_STANDARD,
|
|
UTZNM_INDEX_SHORT_DAYLIGHT,
|
|
UTZNM_INDEX_COUNT
|
|
};
|
|
static const UChar* const EMPTY_NAMES[UTZNM_INDEX_COUNT] = {0,0,0,0,0,0,0};
|
|
|
|
U_CDECL_BEGIN
|
|
static UBool U_CALLCONV tzdbTimeZoneNames_cleanup(void) {
|
|
if (gTZDBNamesMap != NULL) {
|
|
uhash_close(gTZDBNamesMap);
|
|
gTZDBNamesMap = NULL;
|
|
}
|
|
gTZDBNamesMapInitOnce.reset();
|
|
|
|
if (gTZDBNamesTrie != NULL) {
|
|
delete gTZDBNamesTrie;
|
|
gTZDBNamesTrie = NULL;
|
|
}
|
|
gTZDBNamesTrieInitOnce.reset();
|
|
|
|
return true;
|
|
}
|
|
U_CDECL_END
|
|
|
|
/**
|
|
* ZNameInfo stores zone name information in the trie
|
|
*/
|
|
struct ZNameInfo {
|
|
UTimeZoneNameType type;
|
|
const UChar* tzID;
|
|
const UChar* mzID;
|
|
};
|
|
|
|
/**
|
|
* ZMatchInfo stores zone name match information used by find method
|
|
*/
|
|
struct ZMatchInfo {
|
|
const ZNameInfo* znameInfo;
|
|
int32_t matchLength;
|
|
};
|
|
|
|
// Helper functions
|
|
static void mergeTimeZoneKey(const UnicodeString& mzID, char* result);
|
|
|
|
#define DEFAULT_CHARACTERNODE_CAPACITY 1
|
|
|
|
// ---------------------------------------------------
|
|
// CharacterNode class implementation
|
|
// ---------------------------------------------------
|
|
void CharacterNode::clear() {
|
|
uprv_memset(this, 0, sizeof(*this));
|
|
}
|
|
|
|
void CharacterNode::deleteValues(UObjectDeleter *valueDeleter) {
|
|
if (fValues == NULL) {
|
|
// Do nothing.
|
|
} else if (!fHasValuesVector) {
|
|
if (valueDeleter) {
|
|
valueDeleter(fValues);
|
|
}
|
|
} else {
|
|
delete (UVector *)fValues;
|
|
}
|
|
}
|
|
|
|
void
|
|
CharacterNode::addValue(void *value, UObjectDeleter *valueDeleter, UErrorCode &status) {
|
|
if (U_FAILURE(status)) {
|
|
if (valueDeleter) {
|
|
valueDeleter(value);
|
|
}
|
|
return;
|
|
}
|
|
if (fValues == NULL) {
|
|
fValues = value;
|
|
} else {
|
|
// At least one value already.
|
|
if (!fHasValuesVector) {
|
|
// There is only one value so far, and not in a vector yet.
|
|
// Create a vector and add the old value.
|
|
LocalPointer<UVector> values(
|
|
new UVector(valueDeleter, NULL, DEFAULT_CHARACTERNODE_CAPACITY, status), status);
|
|
if (U_FAILURE(status)) {
|
|
if (valueDeleter) {
|
|
valueDeleter(value);
|
|
}
|
|
return;
|
|
}
|
|
if (values->hasDeleter()) {
|
|
values->adoptElement(fValues, status);
|
|
} else {
|
|
values->addElement(fValues, status);
|
|
}
|
|
fValues = values.orphan();
|
|
fHasValuesVector = true;
|
|
}
|
|
// Add the new value.
|
|
UVector *values = (UVector *)fValues;
|
|
if (values->hasDeleter()) {
|
|
values->adoptElement(value, status);
|
|
} else {
|
|
values->addElement(value, status);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------
|
|
// TextTrieMapSearchResultHandler class implementation
|
|
// ---------------------------------------------------
|
|
TextTrieMapSearchResultHandler::~TextTrieMapSearchResultHandler(){
|
|
}
|
|
|
|
// ---------------------------------------------------
|
|
// TextTrieMap class implementation
|
|
// ---------------------------------------------------
|
|
TextTrieMap::TextTrieMap(UBool ignoreCase, UObjectDeleter *valueDeleter)
|
|
: fIgnoreCase(ignoreCase), fNodes(NULL), fNodesCapacity(0), fNodesCount(0),
|
|
fLazyContents(NULL), fIsEmpty(true), fValueDeleter(valueDeleter) {
|
|
}
|
|
|
|
TextTrieMap::~TextTrieMap() {
|
|
int32_t index;
|
|
for (index = 0; index < fNodesCount; ++index) {
|
|
fNodes[index].deleteValues(fValueDeleter);
|
|
}
|
|
uprv_free(fNodes);
|
|
if (fLazyContents != NULL) {
|
|
for (int32_t i=0; i<fLazyContents->size(); i+=2) {
|
|
if (fValueDeleter) {
|
|
fValueDeleter(fLazyContents->elementAt(i+1));
|
|
}
|
|
}
|
|
delete fLazyContents;
|
|
}
|
|
}
|
|
|
|
int32_t TextTrieMap::isEmpty() const {
|
|
// Use a separate field for fIsEmpty because it will remain unchanged once the
|
|
// Trie is built, while fNodes and fLazyContents change with the lazy init
|
|
// of the nodes structure. Trying to test the changing fields has
|
|
// thread safety complications.
|
|
return fIsEmpty;
|
|
}
|
|
|
|
|
|
// We defer actually building the TextTrieMap node structure until the first time a
|
|
// search is performed. put() simply saves the parameters in case we do
|
|
// eventually need to build it.
|
|
//
|
|
void
|
|
TextTrieMap::put(const UnicodeString &key, void *value, ZNStringPool &sp, UErrorCode &status) {
|
|
const UChar *s = sp.get(key, status);
|
|
put(s, value, status);
|
|
}
|
|
|
|
// This method is designed for a persistent key, such as string key stored in
|
|
// resource bundle.
|
|
void
|
|
TextTrieMap::put(const UChar *key, void *value, UErrorCode &status) {
|
|
fIsEmpty = false;
|
|
if (fLazyContents == NULL) {
|
|
LocalPointer<UVector> lpLazyContents(new UVector(status), status);
|
|
fLazyContents = lpLazyContents.orphan();
|
|
}
|
|
if (U_FAILURE(status)) {
|
|
if (fValueDeleter) {
|
|
fValueDeleter((void*) key);
|
|
}
|
|
return;
|
|
}
|
|
U_ASSERT(fLazyContents != NULL);
|
|
|
|
UChar *s = const_cast<UChar *>(key);
|
|
fLazyContents->addElement(s, status);
|
|
if (U_FAILURE(status)) {
|
|
if (fValueDeleter) {
|
|
fValueDeleter((void*) key);
|
|
}
|
|
return;
|
|
}
|
|
|
|
fLazyContents->addElement(value, status);
|
|
}
|
|
|
|
void
|
|
TextTrieMap::putImpl(const UnicodeString &key, void *value, UErrorCode &status) {
|
|
if (fNodes == NULL) {
|
|
fNodesCapacity = 512;
|
|
fNodes = (CharacterNode *)uprv_malloc(fNodesCapacity * sizeof(CharacterNode));
|
|
if (fNodes == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return;
|
|
}
|
|
fNodes[0].clear(); // Init root node.
|
|
fNodesCount = 1;
|
|
}
|
|
|
|
UnicodeString foldedKey;
|
|
const UChar *keyBuffer;
|
|
int32_t keyLength;
|
|
if (fIgnoreCase) {
|
|
// Ok to use fastCopyFrom() because we discard the copy when we return.
|
|
foldedKey.fastCopyFrom(key).foldCase();
|
|
keyBuffer = foldedKey.getBuffer();
|
|
keyLength = foldedKey.length();
|
|
} else {
|
|
keyBuffer = key.getBuffer();
|
|
keyLength = key.length();
|
|
}
|
|
|
|
CharacterNode *node = fNodes;
|
|
int32_t index;
|
|
for (index = 0; index < keyLength; ++index) {
|
|
node = addChildNode(node, keyBuffer[index], status);
|
|
}
|
|
node->addValue(value, fValueDeleter, status);
|
|
}
|
|
|
|
UBool
|
|
TextTrieMap::growNodes() {
|
|
if (fNodesCapacity == 0xffff) {
|
|
return false; // We use 16-bit node indexes.
|
|
}
|
|
int32_t newCapacity = fNodesCapacity + 1000;
|
|
if (newCapacity > 0xffff) {
|
|
newCapacity = 0xffff;
|
|
}
|
|
CharacterNode *newNodes = (CharacterNode *)uprv_malloc(newCapacity * sizeof(CharacterNode));
|
|
if (newNodes == NULL) {
|
|
return false;
|
|
}
|
|
uprv_memcpy(newNodes, fNodes, fNodesCount * sizeof(CharacterNode));
|
|
uprv_free(fNodes);
|
|
fNodes = newNodes;
|
|
fNodesCapacity = newCapacity;
|
|
return true;
|
|
}
|
|
|
|
CharacterNode*
|
|
TextTrieMap::addChildNode(CharacterNode *parent, UChar c, UErrorCode &status) {
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
// Linear search of the sorted list of children.
|
|
uint16_t prevIndex = 0;
|
|
uint16_t nodeIndex = parent->fFirstChild;
|
|
while (nodeIndex > 0) {
|
|
CharacterNode *current = fNodes + nodeIndex;
|
|
UChar childCharacter = current->fCharacter;
|
|
if (childCharacter == c) {
|
|
return current;
|
|
} else if (childCharacter > c) {
|
|
break;
|
|
}
|
|
prevIndex = nodeIndex;
|
|
nodeIndex = current->fNextSibling;
|
|
}
|
|
|
|
// Ensure capacity. Grow fNodes[] if needed.
|
|
if (fNodesCount == fNodesCapacity) {
|
|
int32_t parentIndex = (int32_t)(parent - fNodes);
|
|
if (!growNodes()) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return NULL;
|
|
}
|
|
parent = fNodes + parentIndex;
|
|
}
|
|
|
|
// Insert a new child node with c in sorted order.
|
|
CharacterNode *node = fNodes + fNodesCount;
|
|
node->clear();
|
|
node->fCharacter = c;
|
|
node->fNextSibling = nodeIndex;
|
|
if (prevIndex == 0) {
|
|
parent->fFirstChild = (uint16_t)fNodesCount;
|
|
} else {
|
|
fNodes[prevIndex].fNextSibling = (uint16_t)fNodesCount;
|
|
}
|
|
++fNodesCount;
|
|
return node;
|
|
}
|
|
|
|
CharacterNode*
|
|
TextTrieMap::getChildNode(CharacterNode *parent, UChar c) const {
|
|
// Linear search of the sorted list of children.
|
|
uint16_t nodeIndex = parent->fFirstChild;
|
|
while (nodeIndex > 0) {
|
|
CharacterNode *current = fNodes + nodeIndex;
|
|
UChar childCharacter = current->fCharacter;
|
|
if (childCharacter == c) {
|
|
return current;
|
|
} else if (childCharacter > c) {
|
|
break;
|
|
}
|
|
nodeIndex = current->fNextSibling;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// buildTrie() - The Trie node structure is needed. Create it from the data that was
|
|
// saved at the time the ZoneStringFormatter was created. The Trie is only
|
|
// needed for parsing operations, which are less common than formatting,
|
|
// and the Trie is big, which is why its creation is deferred until first use.
|
|
void TextTrieMap::buildTrie(UErrorCode &status) {
|
|
if (fLazyContents != NULL) {
|
|
for (int32_t i=0; i<fLazyContents->size(); i+=2) {
|
|
const UChar *key = (UChar *)fLazyContents->elementAt(i);
|
|
void *val = fLazyContents->elementAt(i+1);
|
|
UnicodeString keyString(true, key, -1); // Aliasing UnicodeString constructor.
|
|
putImpl(keyString, val, status);
|
|
}
|
|
delete fLazyContents;
|
|
fLazyContents = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
TextTrieMap::search(const UnicodeString &text, int32_t start,
|
|
TextTrieMapSearchResultHandler *handler, UErrorCode &status) const {
|
|
{
|
|
// TODO: if locking the mutex for each check proves to be a performance problem,
|
|
// add a flag of type atomic_int32_t to class TextTrieMap, and use only
|
|
// the ICU atomic safe functions for assigning and testing.
|
|
// Don't test the pointer fLazyContents.
|
|
// Don't do unless it's really required.
|
|
|
|
// Mutex for protecting the lazy creation of the Trie node structure on the first call to search().
|
|
static UMutex TextTrieMutex;
|
|
|
|
Mutex lock(&TextTrieMutex);
|
|
if (fLazyContents != NULL) {
|
|
TextTrieMap *nonConstThis = const_cast<TextTrieMap *>(this);
|
|
nonConstThis->buildTrie(status);
|
|
}
|
|
}
|
|
if (fNodes == NULL) {
|
|
return;
|
|
}
|
|
search(fNodes, text, start, start, handler, status);
|
|
}
|
|
|
|
void
|
|
TextTrieMap::search(CharacterNode *node, const UnicodeString &text, int32_t start,
|
|
int32_t index, TextTrieMapSearchResultHandler *handler, UErrorCode &status) const {
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
if (node->hasValues()) {
|
|
if (!handler->handleMatch(index - start, node, status)) {
|
|
return;
|
|
}
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
}
|
|
if (fIgnoreCase) {
|
|
// for folding we need to get a complete code point.
|
|
// size of character may grow after fold operation;
|
|
// then we need to get result as UTF16 code units.
|
|
UChar32 c32 = text.char32At(index);
|
|
index += U16_LENGTH(c32);
|
|
UnicodeString tmp(c32);
|
|
tmp.foldCase();
|
|
int32_t tmpidx = 0;
|
|
while (tmpidx < tmp.length()) {
|
|
UChar c = tmp.charAt(tmpidx++);
|
|
node = getChildNode(node, c);
|
|
if (node == NULL) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// here we just get the next UTF16 code unit
|
|
UChar c = text.charAt(index++);
|
|
node = getChildNode(node, c);
|
|
}
|
|
if (node != NULL) {
|
|
search(node, text, start, index, handler, status);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------
|
|
// ZNStringPool class implementation
|
|
// ---------------------------------------------------
|
|
static const int32_t POOL_CHUNK_SIZE = 2000;
|
|
struct ZNStringPoolChunk: public UMemory {
|
|
ZNStringPoolChunk *fNext; // Ptr to next pool chunk
|
|
int32_t fLimit; // Index to start of unused area at end of fStrings
|
|
UChar fStrings[POOL_CHUNK_SIZE]; // Strings array
|
|
ZNStringPoolChunk();
|
|
};
|
|
|
|
ZNStringPoolChunk::ZNStringPoolChunk() {
|
|
fNext = NULL;
|
|
fLimit = 0;
|
|
}
|
|
|
|
ZNStringPool::ZNStringPool(UErrorCode &status) {
|
|
fChunks = NULL;
|
|
fHash = NULL;
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
fChunks = new ZNStringPoolChunk;
|
|
if (fChunks == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return;
|
|
}
|
|
|
|
fHash = uhash_open(uhash_hashUChars /* keyHash */,
|
|
uhash_compareUChars /* keyComp */,
|
|
uhash_compareUChars /* valueComp */,
|
|
&status);
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
ZNStringPool::~ZNStringPool() {
|
|
if (fHash != NULL) {
|
|
uhash_close(fHash);
|
|
fHash = NULL;
|
|
}
|
|
|
|
while (fChunks != NULL) {
|
|
ZNStringPoolChunk *nextChunk = fChunks->fNext;
|
|
delete fChunks;
|
|
fChunks = nextChunk;
|
|
}
|
|
}
|
|
|
|
static const UChar EmptyString = 0;
|
|
|
|
const UChar *ZNStringPool::get(const UChar *s, UErrorCode &status) {
|
|
const UChar *pooledString;
|
|
if (U_FAILURE(status)) {
|
|
return &EmptyString;
|
|
}
|
|
|
|
pooledString = static_cast<UChar *>(uhash_get(fHash, s));
|
|
if (pooledString != NULL) {
|
|
return pooledString;
|
|
}
|
|
|
|
int32_t length = u_strlen(s);
|
|
int32_t remainingLength = POOL_CHUNK_SIZE - fChunks->fLimit;
|
|
if (remainingLength <= length) {
|
|
U_ASSERT(length < POOL_CHUNK_SIZE);
|
|
if (length >= POOL_CHUNK_SIZE) {
|
|
status = U_INTERNAL_PROGRAM_ERROR;
|
|
return &EmptyString;
|
|
}
|
|
ZNStringPoolChunk *oldChunk = fChunks;
|
|
fChunks = new ZNStringPoolChunk;
|
|
if (fChunks == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return &EmptyString;
|
|
}
|
|
fChunks->fNext = oldChunk;
|
|
}
|
|
|
|
UChar *destString = &fChunks->fStrings[fChunks->fLimit];
|
|
u_strcpy(destString, s);
|
|
fChunks->fLimit += (length + 1);
|
|
uhash_put(fHash, destString, destString, &status);
|
|
return destString;
|
|
}
|
|
|
|
|
|
//
|
|
// ZNStringPool::adopt() Put a string into the hash, but do not copy the string data
|
|
// into the pool's storage. Used for strings from resource bundles,
|
|
// which will persist for the life of the zone string formatter, and
|
|
// therefore can be used directly without copying.
|
|
const UChar *ZNStringPool::adopt(const UChar * s, UErrorCode &status) {
|
|
const UChar *pooledString;
|
|
if (U_FAILURE(status)) {
|
|
return &EmptyString;
|
|
}
|
|
if (s != NULL) {
|
|
pooledString = static_cast<UChar *>(uhash_get(fHash, s));
|
|
if (pooledString == NULL) {
|
|
UChar *ncs = const_cast<UChar *>(s);
|
|
uhash_put(fHash, ncs, ncs, &status);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
|
|
const UChar *ZNStringPool::get(const UnicodeString &s, UErrorCode &status) {
|
|
UnicodeString &nonConstStr = const_cast<UnicodeString &>(s);
|
|
return this->get(nonConstStr.getTerminatedBuffer(), status);
|
|
}
|
|
|
|
/*
|
|
* freeze(). Close the hash table that maps to the pooled strings.
|
|
* After freezing, the pool can not be searched or added to,
|
|
* but all existing references to pooled strings remain valid.
|
|
*
|
|
* The main purpose is to recover the storage used for the hash.
|
|
*/
|
|
void ZNStringPool::freeze() {
|
|
uhash_close(fHash);
|
|
fHash = NULL;
|
|
}
|
|
|
|
|
|
/**
|
|
* This class stores name data for a meta zone or time zone.
|
|
*/
|
|
class ZNames : public UMemory {
|
|
private:
|
|
friend class TimeZoneNamesImpl;
|
|
|
|
static UTimeZoneNameTypeIndex getTZNameTypeIndex(UTimeZoneNameType type) {
|
|
switch(type) {
|
|
case UTZNM_EXEMPLAR_LOCATION: return UTZNM_INDEX_EXEMPLAR_LOCATION;
|
|
case UTZNM_LONG_GENERIC: return UTZNM_INDEX_LONG_GENERIC;
|
|
case UTZNM_LONG_STANDARD: return UTZNM_INDEX_LONG_STANDARD;
|
|
case UTZNM_LONG_DAYLIGHT: return UTZNM_INDEX_LONG_DAYLIGHT;
|
|
case UTZNM_SHORT_GENERIC: return UTZNM_INDEX_SHORT_GENERIC;
|
|
case UTZNM_SHORT_STANDARD: return UTZNM_INDEX_SHORT_STANDARD;
|
|
case UTZNM_SHORT_DAYLIGHT: return UTZNM_INDEX_SHORT_DAYLIGHT;
|
|
default: return UTZNM_INDEX_UNKNOWN;
|
|
}
|
|
}
|
|
static UTimeZoneNameType getTZNameType(UTimeZoneNameTypeIndex index) {
|
|
switch(index) {
|
|
case UTZNM_INDEX_EXEMPLAR_LOCATION: return UTZNM_EXEMPLAR_LOCATION;
|
|
case UTZNM_INDEX_LONG_GENERIC: return UTZNM_LONG_GENERIC;
|
|
case UTZNM_INDEX_LONG_STANDARD: return UTZNM_LONG_STANDARD;
|
|
case UTZNM_INDEX_LONG_DAYLIGHT: return UTZNM_LONG_DAYLIGHT;
|
|
case UTZNM_INDEX_SHORT_GENERIC: return UTZNM_SHORT_GENERIC;
|
|
case UTZNM_INDEX_SHORT_STANDARD: return UTZNM_SHORT_STANDARD;
|
|
case UTZNM_INDEX_SHORT_DAYLIGHT: return UTZNM_SHORT_DAYLIGHT;
|
|
default: return UTZNM_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
const UChar* fNames[UTZNM_INDEX_COUNT];
|
|
UBool fDidAddIntoTrie;
|
|
|
|
// Whether we own the location string, if computed rather than loaded from a bundle.
|
|
// A meta zone names instance never has an exemplar location string.
|
|
UBool fOwnsLocationName;
|
|
|
|
ZNames(const UChar* names[], const UChar* locationName)
|
|
: fDidAddIntoTrie(false) {
|
|
uprv_memcpy(fNames, names, sizeof(fNames));
|
|
if (locationName != NULL) {
|
|
fOwnsLocationName = true;
|
|
fNames[UTZNM_INDEX_EXEMPLAR_LOCATION] = locationName;
|
|
} else {
|
|
fOwnsLocationName = false;
|
|
}
|
|
}
|
|
|
|
public:
|
|
~ZNames() {
|
|
if (fOwnsLocationName) {
|
|
const UChar* locationName = fNames[UTZNM_INDEX_EXEMPLAR_LOCATION];
|
|
U_ASSERT(locationName != NULL);
|
|
uprv_free((void*) locationName);
|
|
}
|
|
}
|
|
|
|
private:
|
|
static void* createMetaZoneAndPutInCache(UHashtable* cache, const UChar* names[],
|
|
const UnicodeString& mzID, UErrorCode& status) {
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
U_ASSERT(names != NULL);
|
|
|
|
// Use the persistent ID as the resource key, so we can
|
|
// avoid duplications.
|
|
// TODO: Is there a more efficient way, like intern() in Java?
|
|
void* key = (void*) ZoneMeta::findMetaZoneID(mzID);
|
|
void* value;
|
|
if (uprv_memcmp(names, EMPTY_NAMES, sizeof(EMPTY_NAMES)) == 0) {
|
|
value = (void*) EMPTY;
|
|
} else {
|
|
value = (void*) (new ZNames(names, NULL));
|
|
if (value == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return NULL;
|
|
}
|
|
}
|
|
uhash_put(cache, key, value, &status);
|
|
return value;
|
|
}
|
|
|
|
static void* createTimeZoneAndPutInCache(UHashtable* cache, const UChar* names[],
|
|
const UnicodeString& tzID, UErrorCode& status) {
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
U_ASSERT(names != NULL);
|
|
|
|
// If necessary, compute the location name from the time zone name.
|
|
UChar* locationName = NULL;
|
|
if (names[UTZNM_INDEX_EXEMPLAR_LOCATION] == NULL) {
|
|
UnicodeString locationNameUniStr;
|
|
TimeZoneNamesImpl::getDefaultExemplarLocationName(tzID, locationNameUniStr);
|
|
|
|
// Copy the computed location name to the heap
|
|
if (locationNameUniStr.length() > 0) {
|
|
const UChar* buff = locationNameUniStr.getTerminatedBuffer();
|
|
int32_t len = sizeof(UChar) * (locationNameUniStr.length() + 1);
|
|
locationName = (UChar*) uprv_malloc(len);
|
|
if (locationName == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return NULL;
|
|
}
|
|
uprv_memcpy(locationName, buff, len);
|
|
}
|
|
}
|
|
|
|
// Use the persistent ID as the resource key, so we can
|
|
// avoid duplications.
|
|
// TODO: Is there a more efficient way, like intern() in Java?
|
|
void* key = (void*) ZoneMeta::findTimeZoneID(tzID);
|
|
void* value = (void*) (new ZNames(names, locationName));
|
|
if (value == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return NULL;
|
|
}
|
|
uhash_put(cache, key, value, &status);
|
|
return value;
|
|
}
|
|
|
|
const UChar* getName(UTimeZoneNameType type) const {
|
|
UTimeZoneNameTypeIndex index = getTZNameTypeIndex(type);
|
|
return index >= 0 ? fNames[index] : NULL;
|
|
}
|
|
|
|
void addAsMetaZoneIntoTrie(const UChar* mzID, TextTrieMap& trie, UErrorCode& status) {
|
|
addNamesIntoTrie(mzID, NULL, trie, status);
|
|
}
|
|
void addAsTimeZoneIntoTrie(const UChar* tzID, TextTrieMap& trie, UErrorCode& status) {
|
|
addNamesIntoTrie(NULL, tzID, trie, status);
|
|
}
|
|
|
|
void addNamesIntoTrie(const UChar* mzID, const UChar* tzID, TextTrieMap& trie,
|
|
UErrorCode& status) {
|
|
if (U_FAILURE(status)) { return; }
|
|
if (fDidAddIntoTrie) { return; }
|
|
fDidAddIntoTrie = true;
|
|
|
|
for (int32_t i = 0; i < UTZNM_INDEX_COUNT; i++) {
|
|
const UChar* name = fNames[i];
|
|
if (name != NULL) {
|
|
ZNameInfo *nameinfo = (ZNameInfo *)uprv_malloc(sizeof(ZNameInfo));
|
|
if (nameinfo == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return;
|
|
}
|
|
nameinfo->mzID = mzID;
|
|
nameinfo->tzID = tzID;
|
|
nameinfo->type = getTZNameType((UTimeZoneNameTypeIndex)i);
|
|
trie.put(name, nameinfo, status); // trie.put() takes ownership of the key
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
struct ZNamesLoader;
|
|
};
|
|
|
|
struct ZNames::ZNamesLoader : public ResourceSink {
|
|
const UChar *names[UTZNM_INDEX_COUNT];
|
|
|
|
ZNamesLoader() {
|
|
clear();
|
|
}
|
|
virtual ~ZNamesLoader();
|
|
|
|
/** Reset for loading another set of names. */
|
|
void clear() {
|
|
uprv_memcpy(names, EMPTY_NAMES, sizeof(names));
|
|
}
|
|
|
|
void loadMetaZone(const UResourceBundle* zoneStrings, const UnicodeString& mzID, UErrorCode& errorCode) {
|
|
if (U_FAILURE(errorCode)) { return; }
|
|
|
|
char key[ZID_KEY_MAX + 1];
|
|
mergeTimeZoneKey(mzID, key);
|
|
|
|
loadNames(zoneStrings, key, errorCode);
|
|
}
|
|
|
|
void loadTimeZone(const UResourceBundle* zoneStrings, const UnicodeString& tzID, UErrorCode& errorCode) {
|
|
// Replace "/" with ":".
|
|
UnicodeString uKey(tzID);
|
|
for (int32_t i = 0; i < uKey.length(); i++) {
|
|
if (uKey.charAt(i) == (UChar)0x2F) {
|
|
uKey.setCharAt(i, (UChar)0x3A);
|
|
}
|
|
}
|
|
|
|
char key[ZID_KEY_MAX + 1];
|
|
uKey.extract(0, uKey.length(), key, sizeof(key), US_INV);
|
|
|
|
loadNames(zoneStrings, key, errorCode);
|
|
}
|
|
|
|
void loadNames(const UResourceBundle* zoneStrings, const char* key, UErrorCode& errorCode) {
|
|
U_ASSERT(zoneStrings != NULL);
|
|
U_ASSERT(key != NULL);
|
|
U_ASSERT(key[0] != '\0');
|
|
|
|
UErrorCode localStatus = U_ZERO_ERROR;
|
|
clear();
|
|
ures_getAllItemsWithFallback(zoneStrings, key, *this, localStatus);
|
|
|
|
// Ignore errors, but propagate possible warnings.
|
|
if (U_SUCCESS(localStatus)) {
|
|
errorCode = localStatus;
|
|
}
|
|
}
|
|
|
|
void setNameIfEmpty(const char* key, const ResourceValue* value, UErrorCode& errorCode) {
|
|
UTimeZoneNameTypeIndex type = nameTypeFromKey(key);
|
|
if (type == UTZNM_INDEX_UNKNOWN) { return; }
|
|
if (names[type] == NULL) {
|
|
int32_t length;
|
|
// 'NO_NAME' indicates internally that this field should remain empty. It will be
|
|
// replaced by 'NULL' in getNames()
|
|
names[type] = (value == NULL) ? NO_NAME : value->getString(length, errorCode);
|
|
}
|
|
}
|
|
|
|
virtual void put(const char* key, ResourceValue& value, UBool /*noFallback*/,
|
|
UErrorCode &errorCode) override {
|
|
ResourceTable namesTable = value.getTable(errorCode);
|
|
if (U_FAILURE(errorCode)) { return; }
|
|
for (int32_t i = 0; namesTable.getKeyAndValue(i, key, value); ++i) {
|
|
if (value.isNoInheritanceMarker()) {
|
|
setNameIfEmpty(key, NULL, errorCode);
|
|
} else {
|
|
setNameIfEmpty(key, &value, errorCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
static UTimeZoneNameTypeIndex nameTypeFromKey(const char *key) {
|
|
char c0, c1;
|
|
if ((c0 = key[0]) == 0 || (c1 = key[1]) == 0 || key[2] != 0) {
|
|
return UTZNM_INDEX_UNKNOWN;
|
|
}
|
|
if (c0 == 'l') {
|
|
return c1 == 'g' ? UTZNM_INDEX_LONG_GENERIC :
|
|
c1 == 's' ? UTZNM_INDEX_LONG_STANDARD :
|
|
c1 == 'd' ? UTZNM_INDEX_LONG_DAYLIGHT : UTZNM_INDEX_UNKNOWN;
|
|
} else if (c0 == 's') {
|
|
return c1 == 'g' ? UTZNM_INDEX_SHORT_GENERIC :
|
|
c1 == 's' ? UTZNM_INDEX_SHORT_STANDARD :
|
|
c1 == 'd' ? UTZNM_INDEX_SHORT_DAYLIGHT : UTZNM_INDEX_UNKNOWN;
|
|
} else if (c0 == 'e' && c1 == 'c') {
|
|
return UTZNM_INDEX_EXEMPLAR_LOCATION;
|
|
}
|
|
return UTZNM_INDEX_UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of names. It is the caller's responsibility to copy the data into a
|
|
* permanent location, as the returned array is owned by the loader instance and may be
|
|
* cleared or leave scope.
|
|
*
|
|
* This is different than Java, where the array will no longer be modified and null
|
|
* may be returned.
|
|
*/
|
|
const UChar** getNames() {
|
|
// Remove 'NO_NAME' references in the array and replace with 'NULL'
|
|
for (int32_t i = 0; i < UTZNM_INDEX_COUNT; ++i) {
|
|
if (names[i] == NO_NAME) {
|
|
names[i] = NULL;
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
};
|
|
|
|
ZNames::ZNamesLoader::~ZNamesLoader() {}
|
|
|
|
|
|
// ---------------------------------------------------
|
|
// The meta zone ID enumeration class
|
|
// ---------------------------------------------------
|
|
class MetaZoneIDsEnumeration : public StringEnumeration {
|
|
public:
|
|
MetaZoneIDsEnumeration();
|
|
MetaZoneIDsEnumeration(const UVector& mzIDs);
|
|
MetaZoneIDsEnumeration(LocalPointer<UVector> mzIDs);
|
|
virtual ~MetaZoneIDsEnumeration();
|
|
static UClassID U_EXPORT2 getStaticClassID(void);
|
|
virtual UClassID getDynamicClassID(void) const override;
|
|
virtual const UnicodeString* snext(UErrorCode& status) override;
|
|
virtual void reset(UErrorCode& status) override;
|
|
virtual int32_t count(UErrorCode& status) const override;
|
|
private:
|
|
int32_t fLen;
|
|
int32_t fPos;
|
|
const UVector* fMetaZoneIDs;
|
|
LocalPointer<UVector> fLocalVector;
|
|
};
|
|
|
|
UOBJECT_DEFINE_RTTI_IMPLEMENTATION(MetaZoneIDsEnumeration)
|
|
|
|
MetaZoneIDsEnumeration::MetaZoneIDsEnumeration()
|
|
: fLen(0), fPos(0), fMetaZoneIDs(NULL), fLocalVector(NULL) {
|
|
}
|
|
|
|
MetaZoneIDsEnumeration::MetaZoneIDsEnumeration(const UVector& mzIDs)
|
|
: fPos(0), fMetaZoneIDs(&mzIDs), fLocalVector(NULL) {
|
|
fLen = fMetaZoneIDs->size();
|
|
}
|
|
|
|
MetaZoneIDsEnumeration::MetaZoneIDsEnumeration(LocalPointer<UVector> mzIDs)
|
|
: fLen(0), fPos(0), fMetaZoneIDs(nullptr), fLocalVector(std::move(mzIDs)) {
|
|
fMetaZoneIDs = fLocalVector.getAlias();
|
|
if (fMetaZoneIDs) {
|
|
fLen = fMetaZoneIDs->size();
|
|
}
|
|
}
|
|
|
|
const UnicodeString*
|
|
MetaZoneIDsEnumeration::snext(UErrorCode& status) {
|
|
if (U_SUCCESS(status) && fMetaZoneIDs != NULL && fPos < fLen) {
|
|
unistr.setTo((const UChar*)fMetaZoneIDs->elementAt(fPos++), -1);
|
|
return &unistr;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
MetaZoneIDsEnumeration::reset(UErrorCode& /*status*/) {
|
|
fPos = 0;
|
|
}
|
|
|
|
int32_t
|
|
MetaZoneIDsEnumeration::count(UErrorCode& /*status*/) const {
|
|
return fLen;
|
|
}
|
|
|
|
MetaZoneIDsEnumeration::~MetaZoneIDsEnumeration() {
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------
|
|
// ZNameSearchHandler
|
|
// ---------------------------------------------------
|
|
class ZNameSearchHandler : public TextTrieMapSearchResultHandler {
|
|
public:
|
|
ZNameSearchHandler(uint32_t types);
|
|
virtual ~ZNameSearchHandler();
|
|
|
|
UBool handleMatch(int32_t matchLength, const CharacterNode *node, UErrorCode &status) override;
|
|
TimeZoneNames::MatchInfoCollection* getMatches(int32_t& maxMatchLen);
|
|
|
|
private:
|
|
uint32_t fTypes;
|
|
int32_t fMaxMatchLen;
|
|
TimeZoneNames::MatchInfoCollection* fResults;
|
|
};
|
|
|
|
ZNameSearchHandler::ZNameSearchHandler(uint32_t types)
|
|
: fTypes(types), fMaxMatchLen(0), fResults(NULL) {
|
|
}
|
|
|
|
ZNameSearchHandler::~ZNameSearchHandler() {
|
|
if (fResults != NULL) {
|
|
delete fResults;
|
|
}
|
|
}
|
|
|
|
UBool
|
|
ZNameSearchHandler::handleMatch(int32_t matchLength, const CharacterNode *node, UErrorCode &status) {
|
|
if (U_FAILURE(status)) {
|
|
return false;
|
|
}
|
|
if (node->hasValues()) {
|
|
int32_t valuesCount = node->countValues();
|
|
for (int32_t i = 0; i < valuesCount; i++) {
|
|
ZNameInfo *nameinfo = (ZNameInfo *)node->getValue(i);
|
|
if (nameinfo == NULL) {
|
|
continue;
|
|
}
|
|
if ((nameinfo->type & fTypes) != 0) {
|
|
// matches a requested type
|
|
if (fResults == NULL) {
|
|
fResults = new TimeZoneNames::MatchInfoCollection();
|
|
if (fResults == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
}
|
|
}
|
|
if (U_SUCCESS(status)) {
|
|
U_ASSERT(fResults != NULL);
|
|
if (nameinfo->tzID) {
|
|
fResults->addZone(nameinfo->type, matchLength, UnicodeString(nameinfo->tzID, -1), status);
|
|
} else {
|
|
U_ASSERT(nameinfo->mzID);
|
|
fResults->addMetaZone(nameinfo->type, matchLength, UnicodeString(nameinfo->mzID, -1), status);
|
|
}
|
|
if (U_SUCCESS(status) && matchLength > fMaxMatchLen) {
|
|
fMaxMatchLen = matchLength;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
TimeZoneNames::MatchInfoCollection*
|
|
ZNameSearchHandler::getMatches(int32_t& maxMatchLen) {
|
|
// give the ownership to the caller
|
|
TimeZoneNames::MatchInfoCollection* results = fResults;
|
|
maxMatchLen = fMaxMatchLen;
|
|
|
|
// reset
|
|
fResults = NULL;
|
|
fMaxMatchLen = 0;
|
|
return results;
|
|
}
|
|
|
|
// ---------------------------------------------------
|
|
// TimeZoneNamesImpl
|
|
//
|
|
// TimeZoneNames implementation class. This is the main
|
|
// part of this module.
|
|
// ---------------------------------------------------
|
|
|
|
U_CDECL_BEGIN
|
|
/**
|
|
* Deleter for ZNames
|
|
*/
|
|
static void U_CALLCONV
|
|
deleteZNames(void *obj) {
|
|
if (obj != EMPTY) {
|
|
delete (ZNames*) obj;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deleter for ZNameInfo
|
|
*/
|
|
static void U_CALLCONV
|
|
deleteZNameInfo(void *obj) {
|
|
uprv_free(obj);
|
|
}
|
|
|
|
U_CDECL_END
|
|
|
|
TimeZoneNamesImpl::TimeZoneNamesImpl(const Locale& locale, UErrorCode& status)
|
|
: fLocale(locale),
|
|
fZoneStrings(NULL),
|
|
fTZNamesMap(NULL),
|
|
fMZNamesMap(NULL),
|
|
fNamesTrieFullyLoaded(false),
|
|
fNamesFullyLoaded(false),
|
|
fNamesTrie(true, deleteZNameInfo) {
|
|
initialize(locale, status);
|
|
}
|
|
|
|
void
|
|
TimeZoneNamesImpl::initialize(const Locale& locale, UErrorCode& status) {
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
|
|
// Load zoneStrings bundle
|
|
UErrorCode tmpsts = U_ZERO_ERROR; // OK with fallback warning..
|
|
fZoneStrings = ures_open(U_ICUDATA_ZONE, locale.getName(), &tmpsts);
|
|
fZoneStrings = ures_getByKeyWithFallback(fZoneStrings, gZoneStrings, fZoneStrings, &tmpsts);
|
|
if (U_FAILURE(tmpsts)) {
|
|
status = tmpsts;
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
// Initialize hashtables holding time zone/meta zone names
|
|
fMZNamesMap = uhash_open(uhash_hashUChars, uhash_compareUChars, NULL, &status);
|
|
fTZNamesMap = uhash_open(uhash_hashUChars, uhash_compareUChars, NULL, &status);
|
|
if (U_FAILURE(status)) {
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
uhash_setValueDeleter(fMZNamesMap, deleteZNames);
|
|
uhash_setValueDeleter(fTZNamesMap, deleteZNames);
|
|
// no key deleters for name maps
|
|
|
|
// preload zone strings for the default zone
|
|
TimeZone *tz = TimeZone::createDefault();
|
|
const UChar *tzID = ZoneMeta::getCanonicalCLDRID(*tz);
|
|
if (tzID != NULL) {
|
|
loadStrings(UnicodeString(tzID), status);
|
|
}
|
|
delete tz;
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* This method updates the cache and must be called with a lock,
|
|
* except initializer.
|
|
*/
|
|
void
|
|
TimeZoneNamesImpl::loadStrings(const UnicodeString& tzCanonicalID, UErrorCode& status) {
|
|
loadTimeZoneNames(tzCanonicalID, status);
|
|
LocalPointer<StringEnumeration> mzIDs(getAvailableMetaZoneIDs(tzCanonicalID, status));
|
|
if (U_FAILURE(status)) { return; }
|
|
U_ASSERT(!mzIDs.isNull());
|
|
|
|
const UnicodeString *mzID;
|
|
while (((mzID = mzIDs->snext(status)) != NULL) && U_SUCCESS(status)) {
|
|
loadMetaZoneNames(*mzID, status);
|
|
}
|
|
}
|
|
|
|
TimeZoneNamesImpl::~TimeZoneNamesImpl() {
|
|
cleanup();
|
|
}
|
|
|
|
void
|
|
TimeZoneNamesImpl::cleanup() {
|
|
if (fZoneStrings != NULL) {
|
|
ures_close(fZoneStrings);
|
|
fZoneStrings = NULL;
|
|
}
|
|
if (fMZNamesMap != NULL) {
|
|
uhash_close(fMZNamesMap);
|
|
fMZNamesMap = NULL;
|
|
}
|
|
if (fTZNamesMap != NULL) {
|
|
uhash_close(fTZNamesMap);
|
|
fTZNamesMap = NULL;
|
|
}
|
|
}
|
|
|
|
bool
|
|
TimeZoneNamesImpl::operator==(const TimeZoneNames& other) const {
|
|
if (this == &other) {
|
|
return true;
|
|
}
|
|
// No implementation for now
|
|
return false;
|
|
}
|
|
|
|
TimeZoneNamesImpl*
|
|
TimeZoneNamesImpl::clone() const {
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
return new TimeZoneNamesImpl(fLocale, status);
|
|
}
|
|
|
|
StringEnumeration*
|
|
TimeZoneNamesImpl::getAvailableMetaZoneIDs(UErrorCode& status) const {
|
|
return TimeZoneNamesImpl::_getAvailableMetaZoneIDs(status);
|
|
}
|
|
|
|
// static implementation of getAvailableMetaZoneIDs(UErrorCode&)
|
|
StringEnumeration*
|
|
TimeZoneNamesImpl::_getAvailableMetaZoneIDs(UErrorCode& status) {
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
const UVector* mzIDs = ZoneMeta::getAvailableMetazoneIDs();
|
|
if (mzIDs == NULL) {
|
|
return new MetaZoneIDsEnumeration();
|
|
}
|
|
return new MetaZoneIDsEnumeration(*mzIDs);
|
|
}
|
|
|
|
StringEnumeration*
|
|
TimeZoneNamesImpl::getAvailableMetaZoneIDs(const UnicodeString& tzID, UErrorCode& status) const {
|
|
return TimeZoneNamesImpl::_getAvailableMetaZoneIDs(tzID, status);
|
|
}
|
|
|
|
// static implementation of getAvailableMetaZoneIDs(const UnicodeString&, UErrorCode&)
|
|
StringEnumeration*
|
|
TimeZoneNamesImpl::_getAvailableMetaZoneIDs(const UnicodeString& tzID, UErrorCode& status) {
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
const UVector* mappings = ZoneMeta::getMetazoneMappings(tzID);
|
|
if (mappings == NULL) {
|
|
return new MetaZoneIDsEnumeration();
|
|
}
|
|
|
|
LocalPointer<MetaZoneIDsEnumeration> senum;
|
|
LocalPointer<UVector> mzIDs(new UVector(NULL, uhash_compareUChars, status), status);
|
|
if (U_SUCCESS(status)) {
|
|
U_ASSERT(mzIDs.isValid());
|
|
for (int32_t i = 0; U_SUCCESS(status) && i < mappings->size(); i++) {
|
|
|
|
OlsonToMetaMappingEntry *map = (OlsonToMetaMappingEntry *)mappings->elementAt(i);
|
|
const UChar *mzID = map->mzid;
|
|
if (!mzIDs->contains((void *)mzID)) {
|
|
mzIDs->addElement((void *)mzID, status);
|
|
}
|
|
}
|
|
if (U_SUCCESS(status)) {
|
|
senum.adoptInsteadAndCheckErrorCode(new MetaZoneIDsEnumeration(std::move(mzIDs)), status);
|
|
}
|
|
}
|
|
return U_SUCCESS(status) ? senum.orphan() : nullptr;
|
|
}
|
|
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::getMetaZoneID(const UnicodeString& tzID, UDate date, UnicodeString& mzID) const {
|
|
return TimeZoneNamesImpl::_getMetaZoneID(tzID, date, mzID);
|
|
}
|
|
|
|
// static implementation of getMetaZoneID
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::_getMetaZoneID(const UnicodeString& tzID, UDate date, UnicodeString& mzID) {
|
|
ZoneMeta::getMetazoneID(tzID, date, mzID);
|
|
return mzID;
|
|
}
|
|
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::getReferenceZoneID(const UnicodeString& mzID, const char* region, UnicodeString& tzID) const {
|
|
return TimeZoneNamesImpl::_getReferenceZoneID(mzID, region, tzID);
|
|
}
|
|
|
|
// static implementation of getReferenceZoneID
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::_getReferenceZoneID(const UnicodeString& mzID, const char* region, UnicodeString& tzID) {
|
|
ZoneMeta::getZoneIdByMetazone(mzID, UnicodeString(region, -1, US_INV), tzID);
|
|
return tzID;
|
|
}
|
|
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::getMetaZoneDisplayName(const UnicodeString& mzID,
|
|
UTimeZoneNameType type,
|
|
UnicodeString& name) const {
|
|
name.setToBogus(); // cleanup result.
|
|
if (mzID.isEmpty()) {
|
|
return name;
|
|
}
|
|
|
|
ZNames *znames = NULL;
|
|
TimeZoneNamesImpl *nonConstThis = const_cast<TimeZoneNamesImpl *>(this);
|
|
|
|
{
|
|
Mutex lock(&gDataMutex);
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
znames = nonConstThis->loadMetaZoneNames(mzID, status);
|
|
if (U_FAILURE(status)) { return name; }
|
|
}
|
|
|
|
if (znames != NULL) {
|
|
const UChar* s = znames->getName(type);
|
|
if (s != NULL) {
|
|
name.setTo(true, s, -1);
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::getTimeZoneDisplayName(const UnicodeString& tzID, UTimeZoneNameType type, UnicodeString& name) const {
|
|
name.setToBogus(); // cleanup result.
|
|
if (tzID.isEmpty()) {
|
|
return name;
|
|
}
|
|
|
|
ZNames *tznames = NULL;
|
|
TimeZoneNamesImpl *nonConstThis = const_cast<TimeZoneNamesImpl *>(this);
|
|
|
|
{
|
|
Mutex lock(&gDataMutex);
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
tznames = nonConstThis->loadTimeZoneNames(tzID, status);
|
|
if (U_FAILURE(status)) { return name; }
|
|
}
|
|
|
|
if (tznames != NULL) {
|
|
const UChar *s = tznames->getName(type);
|
|
if (s != NULL) {
|
|
name.setTo(true, s, -1);
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
UnicodeString&
|
|
TimeZoneNamesImpl::getExemplarLocationName(const UnicodeString& tzID, UnicodeString& name) const {
|
|
name.setToBogus(); // cleanup result.
|
|
const UChar* locName = NULL;
|
|
ZNames *tznames = NULL;
|
|
TimeZoneNamesImpl *nonConstThis = const_cast<TimeZoneNamesImpl *>(this);
|
|
|
|
{
|
|
Mutex lock(&gDataMutex);
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
tznames = nonConstThis->loadTimeZoneNames(tzID, status);
|
|
if (U_FAILURE(status)) { return name; }
|
|
}
|
|
|
|
if (tznames != NULL) {
|
|
locName = tznames->getName(UTZNM_EXEMPLAR_LOCATION);
|
|
}
|
|
if (locName != NULL) {
|
|
name.setTo(true, locName, -1);
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
|
|
// Merge the MZ_PREFIX and mzId
|
|
static void mergeTimeZoneKey(const UnicodeString& mzID, char* result) {
|
|
if (mzID.isEmpty()) {
|
|
result[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
char mzIdChar[ZID_KEY_MAX + 1];
|
|
int32_t keyLen;
|
|
int32_t prefixLen = static_cast<int32_t>(uprv_strlen(gMZPrefix));
|
|
keyLen = mzID.extract(0, mzID.length(), mzIdChar, ZID_KEY_MAX + 1, US_INV);
|
|
uprv_memcpy((void *)result, (void *)gMZPrefix, prefixLen);
|
|
uprv_memcpy((void *)(result + prefixLen), (void *)mzIdChar, keyLen);
|
|
result[keyLen + prefixLen] = '\0';
|
|
}
|
|
|
|
/*
|
|
* This method updates the cache and must be called with a lock
|
|
*/
|
|
ZNames*
|
|
TimeZoneNamesImpl::loadMetaZoneNames(const UnicodeString& mzID, UErrorCode& status) {
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
U_ASSERT(mzID.length() <= ZID_KEY_MAX - MZ_PREFIX_LEN);
|
|
|
|
UChar mzIDKey[ZID_KEY_MAX + 1];
|
|
mzID.extract(mzIDKey, ZID_KEY_MAX + 1, status);
|
|
U_ASSERT(U_SUCCESS(status)); // already checked length above
|
|
mzIDKey[mzID.length()] = 0;
|
|
|
|
void* mznames = uhash_get(fMZNamesMap, mzIDKey);
|
|
if (mznames == NULL) {
|
|
ZNames::ZNamesLoader loader;
|
|
loader.loadMetaZone(fZoneStrings, mzID, status);
|
|
mznames = ZNames::createMetaZoneAndPutInCache(fMZNamesMap, loader.getNames(), mzID, status);
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
}
|
|
|
|
if (mznames != EMPTY) {
|
|
return (ZNames*)mznames;
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This method updates the cache and must be called with a lock
|
|
*/
|
|
ZNames*
|
|
TimeZoneNamesImpl::loadTimeZoneNames(const UnicodeString& tzID, UErrorCode& status) {
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
U_ASSERT(tzID.length() <= ZID_KEY_MAX);
|
|
|
|
UChar tzIDKey[ZID_KEY_MAX + 1];
|
|
int32_t tzIDKeyLen = tzID.extract(tzIDKey, ZID_KEY_MAX + 1, status);
|
|
U_ASSERT(U_SUCCESS(status)); // already checked length above
|
|
tzIDKey[tzIDKeyLen] = 0;
|
|
|
|
void *tznames = uhash_get(fTZNamesMap, tzIDKey);
|
|
if (tznames == NULL) {
|
|
ZNames::ZNamesLoader loader;
|
|
loader.loadTimeZone(fZoneStrings, tzID, status);
|
|
tznames = ZNames::createTimeZoneAndPutInCache(fTZNamesMap, loader.getNames(), tzID, status);
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
}
|
|
|
|
// tznames is never EMPTY
|
|
return (ZNames*)tznames;
|
|
}
|
|
|
|
TimeZoneNames::MatchInfoCollection*
|
|
TimeZoneNamesImpl::find(const UnicodeString& text, int32_t start, uint32_t types, UErrorCode& status) const {
|
|
ZNameSearchHandler handler(types);
|
|
TimeZoneNames::MatchInfoCollection* matches;
|
|
TimeZoneNamesImpl* nonConstThis = const_cast<TimeZoneNamesImpl*>(this);
|
|
|
|
// Synchronize so that data is not loaded multiple times.
|
|
// TODO: Consider more fine-grained synchronization.
|
|
{
|
|
Mutex lock(&gDataMutex);
|
|
|
|
// First try of lookup.
|
|
matches = doFind(handler, text, start, status);
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
if (matches != NULL) {
|
|
return matches;
|
|
}
|
|
|
|
// All names are not yet loaded into the trie.
|
|
// We may have loaded names for formatting several time zones,
|
|
// and might be parsing one of those.
|
|
// Populate the parsing trie from all of the already-loaded names.
|
|
nonConstThis->addAllNamesIntoTrie(status);
|
|
|
|
// Second try of lookup.
|
|
matches = doFind(handler, text, start, status);
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
if (matches != NULL) {
|
|
return matches;
|
|
}
|
|
|
|
// There are still some names we haven't loaded into the trie yet.
|
|
// Load everything now.
|
|
nonConstThis->internalLoadAllDisplayNames(status);
|
|
nonConstThis->addAllNamesIntoTrie(status);
|
|
nonConstThis->fNamesTrieFullyLoaded = true;
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
|
|
// Third try: we must return this one.
|
|
return doFind(handler, text, start, status);
|
|
}
|
|
}
|
|
|
|
TimeZoneNames::MatchInfoCollection*
|
|
TimeZoneNamesImpl::doFind(ZNameSearchHandler& handler,
|
|
const UnicodeString& text, int32_t start, UErrorCode& status) const {
|
|
|
|
fNamesTrie.search(text, start, (TextTrieMapSearchResultHandler *)&handler, status);
|
|
if (U_FAILURE(status)) { return NULL; }
|
|
|
|
int32_t maxLen = 0;
|
|
TimeZoneNames::MatchInfoCollection* matches = handler.getMatches(maxLen);
|
|
if (matches != NULL && ((maxLen == (text.length() - start)) || fNamesTrieFullyLoaded)) {
|
|
// perfect match, or no more names available
|
|
return matches;
|
|
}
|
|
delete matches;
|
|
return NULL;
|
|
}
|
|
|
|
// Caller must synchronize.
|
|
void TimeZoneNamesImpl::addAllNamesIntoTrie(UErrorCode& status) {
|
|
if (U_FAILURE(status)) return;
|
|
int32_t pos;
|
|
const UHashElement* element;
|
|
|
|
pos = UHASH_FIRST;
|
|
while ((element = uhash_nextElement(fMZNamesMap, &pos)) != NULL) {
|
|
if (element->value.pointer == EMPTY) { continue; }
|
|
UChar* mzID = (UChar*) element->key.pointer;
|
|
ZNames* znames = (ZNames*) element->value.pointer;
|
|
znames->addAsMetaZoneIntoTrie(mzID, fNamesTrie, status);
|
|
if (U_FAILURE(status)) { return; }
|
|
}
|
|
|
|
pos = UHASH_FIRST;
|
|
while ((element = uhash_nextElement(fTZNamesMap, &pos)) != NULL) {
|
|
if (element->value.pointer == EMPTY) { continue; }
|
|
UChar* tzID = (UChar*) element->key.pointer;
|
|
ZNames* znames = (ZNames*) element->value.pointer;
|
|
znames->addAsTimeZoneIntoTrie(tzID, fNamesTrie, status);
|
|
if (U_FAILURE(status)) { return; }
|
|
}
|
|
}
|
|
|
|
U_CDECL_BEGIN
|
|
static void U_CALLCONV
|
|
deleteZNamesLoader(void* obj) {
|
|
if (obj == DUMMY_LOADER) { return; }
|
|
const ZNames::ZNamesLoader* loader = (const ZNames::ZNamesLoader*) obj;
|
|
delete loader;
|
|
}
|
|
U_CDECL_END
|
|
|
|
struct TimeZoneNamesImpl::ZoneStringsLoader : public ResourceSink {
|
|
TimeZoneNamesImpl& tzn;
|
|
UHashtable* keyToLoader;
|
|
|
|
ZoneStringsLoader(TimeZoneNamesImpl& _tzn, UErrorCode& status)
|
|
: tzn(_tzn) {
|
|
keyToLoader = uhash_open(uhash_hashChars, uhash_compareChars, NULL, &status);
|
|
if (U_FAILURE(status)) { return; }
|
|
uhash_setKeyDeleter(keyToLoader, uprv_free);
|
|
uhash_setValueDeleter(keyToLoader, deleteZNamesLoader);
|
|
}
|
|
virtual ~ZoneStringsLoader();
|
|
|
|
void* createKey(const char* key, UErrorCode& status) {
|
|
int32_t len = sizeof(char) * (static_cast<int32_t>(uprv_strlen(key)) + 1);
|
|
char* newKey = (char*) uprv_malloc(len);
|
|
if (newKey == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return NULL;
|
|
}
|
|
uprv_memcpy(newKey, key, len);
|
|
newKey[len-1] = '\0';
|
|
return (void*) newKey;
|
|
}
|
|
|
|
UBool isMetaZone(const char* key) {
|
|
return (uprv_strlen(key) >= MZ_PREFIX_LEN && uprv_memcmp(key, gMZPrefix, MZ_PREFIX_LEN) == 0);
|
|
}
|
|
|
|
UnicodeString mzIDFromKey(const char* key) {
|
|
return UnicodeString(key + MZ_PREFIX_LEN, static_cast<int32_t>(uprv_strlen(key)) - MZ_PREFIX_LEN, US_INV);
|
|
}
|
|
|
|
UnicodeString tzIDFromKey(const char* key) {
|
|
UnicodeString tzID(key, -1, US_INV);
|
|
// Replace all colons ':' with slashes '/'
|
|
for (int i=0; i<tzID.length(); i++) {
|
|
if (tzID.charAt(i) == 0x003A) {
|
|
tzID.setCharAt(i, 0x002F);
|
|
}
|
|
}
|
|
return tzID;
|
|
}
|
|
|
|
void load(UErrorCode& status) {
|
|
ures_getAllItemsWithFallback(tzn.fZoneStrings, "", *this, status);
|
|
if (U_FAILURE(status)) { return; }
|
|
|
|
int32_t pos = UHASH_FIRST;
|
|
const UHashElement* element;
|
|
while ((element = uhash_nextElement(keyToLoader, &pos)) != NULL) {
|
|
if (element->value.pointer == DUMMY_LOADER) { continue; }
|
|
ZNames::ZNamesLoader* loader = (ZNames::ZNamesLoader*) element->value.pointer;
|
|
char* key = (char*) element->key.pointer;
|
|
|
|
if (isMetaZone(key)) {
|
|
UnicodeString mzID = mzIDFromKey(key);
|
|
ZNames::createMetaZoneAndPutInCache(tzn.fMZNamesMap, loader->getNames(), mzID, status);
|
|
} else {
|
|
UnicodeString tzID = tzIDFromKey(key);
|
|
ZNames::createTimeZoneAndPutInCache(tzn.fTZNamesMap, loader->getNames(), tzID, status);
|
|
}
|
|
if (U_FAILURE(status)) { return; }
|
|
}
|
|
}
|
|
|
|
void consumeNamesTable(const char *key, ResourceValue &value, UBool noFallback,
|
|
UErrorCode &status) {
|
|
if (U_FAILURE(status)) { return; }
|
|
|
|
void* loader = uhash_get(keyToLoader, key);
|
|
if (loader == NULL) {
|
|
if (isMetaZone(key)) {
|
|
UnicodeString mzID = mzIDFromKey(key);
|
|
void* cacheVal = uhash_get(tzn.fMZNamesMap, mzID.getTerminatedBuffer());
|
|
if (cacheVal != NULL) {
|
|
// We have already loaded the names for this meta zone.
|
|
loader = (void*) DUMMY_LOADER;
|
|
} else {
|
|
loader = (void*) new ZNames::ZNamesLoader();
|
|
if (loader == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
UnicodeString tzID = tzIDFromKey(key);
|
|
void* cacheVal = uhash_get(tzn.fTZNamesMap, tzID.getTerminatedBuffer());
|
|
if (cacheVal != NULL) {
|
|
// We have already loaded the names for this time zone.
|
|
loader = (void*) DUMMY_LOADER;
|
|
} else {
|
|
loader = (void*) new ZNames::ZNamesLoader();
|
|
if (loader == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void* newKey = createKey(key, status);
|
|
if (U_FAILURE(status)) {
|
|
deleteZNamesLoader(loader);
|
|
return;
|
|
}
|
|
|
|
uhash_put(keyToLoader, newKey, loader, &status);
|
|
if (U_FAILURE(status)) { return; }
|
|
}
|
|
|
|
if (loader != DUMMY_LOADER) {
|
|
// Let the ZNamesLoader consume the names table.
|
|
((ZNames::ZNamesLoader*)loader)->put(key, value, noFallback, status);
|
|
}
|
|
}
|
|
|
|
virtual void put(const char *key, ResourceValue &value, UBool noFallback,
|
|
UErrorCode &status) override {
|
|
ResourceTable timeZonesTable = value.getTable(status);
|
|
if (U_FAILURE(status)) { return; }
|
|
for (int32_t i = 0; timeZonesTable.getKeyAndValue(i, key, value); ++i) {
|
|
U_ASSERT(!value.isNoInheritanceMarker());
|
|
if (value.getType() == URES_TABLE) {
|
|
consumeNamesTable(key, value, noFallback, status);
|
|
} else {
|
|
// Ignore fields that aren't tables (e.g., fallbackFormat and regionFormatStandard).
|
|
// All time zone fields are tables.
|
|
}
|
|
if (U_FAILURE(status)) { return; }
|
|
}
|
|
}
|
|
};
|
|
|
|
// Virtual destructors must be defined out of line.
|
|
TimeZoneNamesImpl::ZoneStringsLoader::~ZoneStringsLoader() {
|
|
uhash_close(keyToLoader);
|
|
}
|
|
|
|
void TimeZoneNamesImpl::loadAllDisplayNames(UErrorCode& status) {
|
|
if (U_FAILURE(status)) return;
|
|
|
|
{
|
|
Mutex lock(&gDataMutex);
|
|
internalLoadAllDisplayNames(status);
|
|
}
|
|
}
|
|
|
|
void TimeZoneNamesImpl::getDisplayNames(const UnicodeString& tzID,
|
|
const UTimeZoneNameType types[], int32_t numTypes,
|
|
UDate date, UnicodeString dest[], UErrorCode& status) const {
|
|
if (U_FAILURE(status)) return;
|
|
|
|
if (tzID.isEmpty()) { return; }
|
|
void* tznames = NULL;
|
|
void* mznames = NULL;
|
|
TimeZoneNamesImpl *nonConstThis = const_cast<TimeZoneNamesImpl*>(this);
|
|
|
|
// Load the time zone strings
|
|
{
|
|
Mutex lock(&gDataMutex);
|
|
tznames = (void*) nonConstThis->loadTimeZoneNames(tzID, status);
|
|
if (U_FAILURE(status)) { return; }
|
|
}
|
|
U_ASSERT(tznames != NULL);
|
|
|
|
// Load the values into the dest array
|
|
for (int i = 0; i < numTypes; i++) {
|
|
UTimeZoneNameType type = types[i];
|
|
const UChar* name = ((ZNames*)tznames)->getName(type);
|
|
if (name == NULL) {
|
|
if (mznames == NULL) {
|
|
// Load the meta zone name
|
|
UnicodeString mzID;
|
|
getMetaZoneID(tzID, date, mzID);
|
|
if (mzID.isEmpty()) {
|
|
mznames = (void*) EMPTY;
|
|
} else {
|
|
// Load the meta zone strings
|
|
// Mutex is scoped to the "else" statement
|
|
Mutex lock(&gDataMutex);
|
|
mznames = (void*) nonConstThis->loadMetaZoneNames(mzID, status);
|
|
if (U_FAILURE(status)) { return; }
|
|
// Note: when the metazone doesn't exist, in Java, loadMetaZoneNames returns
|
|
// a dummy object instead of NULL.
|
|
if (mznames == NULL) {
|
|
mznames = (void*) EMPTY;
|
|
}
|
|
}
|
|
}
|
|
U_ASSERT(mznames != NULL);
|
|
if (mznames != EMPTY) {
|
|
name = ((ZNames*)mznames)->getName(type);
|
|
}
|
|
}
|
|
if (name != NULL) {
|
|
dest[i].setTo(true, name, -1);
|
|
} else {
|
|
dest[i].setToBogus();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Caller must synchronize.
|
|
void TimeZoneNamesImpl::internalLoadAllDisplayNames(UErrorCode& status) {
|
|
if (!fNamesFullyLoaded) {
|
|
fNamesFullyLoaded = true;
|
|
|
|
ZoneStringsLoader loader(*this, status);
|
|
loader.load(status);
|
|
if (U_FAILURE(status)) { return; }
|
|
|
|
const UnicodeString *id;
|
|
|
|
// load strings for all zones
|
|
StringEnumeration *tzIDs = TimeZone::createTimeZoneIDEnumeration(
|
|
UCAL_ZONE_TYPE_CANONICAL, NULL, NULL, status);
|
|
if (U_SUCCESS(status)) {
|
|
while ((id = tzIDs->snext(status)) != NULL) {
|
|
if (U_FAILURE(status)) {
|
|
break;
|
|
}
|
|
UnicodeString copy(*id);
|
|
void* value = uhash_get(fTZNamesMap, copy.getTerminatedBuffer());
|
|
if (value == NULL) {
|
|
// loadStrings also loads related metazone strings
|
|
loadStrings(*id, status);
|
|
}
|
|
}
|
|
}
|
|
if (tzIDs != NULL) {
|
|
delete tzIDs;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
static const UChar gEtcPrefix[] = { 0x45, 0x74, 0x63, 0x2F }; // "Etc/"
|
|
static const int32_t gEtcPrefixLen = 4;
|
|
static const UChar gSystemVPrefix[] = { 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x56, 0x2F }; // "SystemV/
|
|
static const int32_t gSystemVPrefixLen = 8;
|
|
static const UChar gRiyadh8[] = { 0x52, 0x69, 0x79, 0x61, 0x64, 0x68, 0x38 }; // "Riyadh8"
|
|
static const int32_t gRiyadh8Len = 7;
|
|
|
|
UnicodeString& U_EXPORT2
|
|
TimeZoneNamesImpl::getDefaultExemplarLocationName(const UnicodeString& tzID, UnicodeString& name) {
|
|
if (tzID.isEmpty() || tzID.startsWith(gEtcPrefix, gEtcPrefixLen)
|
|
|| tzID.startsWith(gSystemVPrefix, gSystemVPrefixLen) || tzID.indexOf(gRiyadh8, gRiyadh8Len, 0) > 0) {
|
|
name.setToBogus();
|
|
return name;
|
|
}
|
|
|
|
int32_t sep = tzID.lastIndexOf((UChar)0x2F /* '/' */);
|
|
if (sep > 0 && sep + 1 < tzID.length()) {
|
|
name.setTo(tzID, sep + 1);
|
|
name.findAndReplace(UnicodeString((UChar)0x5f /* _ */),
|
|
UnicodeString((UChar)0x20 /* space */));
|
|
} else {
|
|
name.setToBogus();
|
|
}
|
|
return name;
|
|
}
|
|
|
|
// ---------------------------------------------------
|
|
// TZDBTimeZoneNames and its supporting classes
|
|
//
|
|
// TZDBTimeZoneNames is an implementation class of
|
|
// TimeZoneNames holding the IANA tz database abbreviations.
|
|
// ---------------------------------------------------
|
|
|
|
class TZDBNames : public UMemory {
|
|
public:
|
|
virtual ~TZDBNames();
|
|
|
|
static TZDBNames* createInstance(UResourceBundle* rb, const char* key);
|
|
const UChar* getName(UTimeZoneNameType type) const;
|
|
const char** getParseRegions(int32_t& numRegions) const;
|
|
|
|
protected:
|
|
TZDBNames(const UChar** names, char** regions, int32_t numRegions);
|
|
|
|
private:
|
|
const UChar** fNames;
|
|
char** fRegions;
|
|
int32_t fNumRegions;
|
|
};
|
|
|
|
TZDBNames::TZDBNames(const UChar** names, char** regions, int32_t numRegions)
|
|
: fNames(names),
|
|
fRegions(regions),
|
|
fNumRegions(numRegions) {
|
|
}
|
|
|
|
TZDBNames::~TZDBNames() {
|
|
if (fNames != NULL) {
|
|
uprv_free(fNames);
|
|
}
|
|
if (fRegions != NULL) {
|
|
char **p = fRegions;
|
|
for (int32_t i = 0; i < fNumRegions; p++, i++) {
|
|
uprv_free(*p);
|
|
}
|
|
uprv_free(fRegions);
|
|
}
|
|
}
|
|
|
|
TZDBNames*
|
|
TZDBNames::createInstance(UResourceBundle* rb, const char* key) {
|
|
if (rb == NULL || key == NULL || *key == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
|
|
const UChar **names = NULL;
|
|
char** regions = NULL;
|
|
int32_t numRegions = 0;
|
|
|
|
int32_t len = 0;
|
|
|
|
UResourceBundle* rbTable = NULL;
|
|
rbTable = ures_getByKey(rb, key, rbTable, &status);
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
|
|
names = (const UChar **)uprv_malloc(sizeof(const UChar*) * TZDBNAMES_KEYS_SIZE);
|
|
UBool isEmpty = true;
|
|
if (names != NULL) {
|
|
for (int32_t i = 0; i < TZDBNAMES_KEYS_SIZE; i++) {
|
|
status = U_ZERO_ERROR;
|
|
const UChar *value = ures_getStringByKey(rbTable, TZDBNAMES_KEYS[i], &len, &status);
|
|
if (U_FAILURE(status) || len == 0) {
|
|
names[i] = NULL;
|
|
} else {
|
|
names[i] = value;
|
|
isEmpty = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isEmpty) {
|
|
if (names != NULL) {
|
|
uprv_free(names);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
UResourceBundle *regionsRes = ures_getByKey(rbTable, "parseRegions", NULL, &status);
|
|
UBool regionError = false;
|
|
if (U_SUCCESS(status)) {
|
|
numRegions = ures_getSize(regionsRes);
|
|
if (numRegions > 0) {
|
|
regions = (char**)uprv_malloc(sizeof(char*) * numRegions);
|
|
if (regions != NULL) {
|
|
char **pRegion = regions;
|
|
for (int32_t i = 0; i < numRegions; i++, pRegion++) {
|
|
*pRegion = NULL;
|
|
}
|
|
// filling regions
|
|
pRegion = regions;
|
|
for (int32_t i = 0; i < numRegions; i++, pRegion++) {
|
|
status = U_ZERO_ERROR;
|
|
const UChar *uregion = ures_getStringByIndex(regionsRes, i, &len, &status);
|
|
if (U_FAILURE(status)) {
|
|
regionError = true;
|
|
break;
|
|
}
|
|
*pRegion = (char*)uprv_malloc(sizeof(char) * (len + 1));
|
|
if (*pRegion == NULL) {
|
|
regionError = true;
|
|
break;
|
|
}
|
|
u_UCharsToChars(uregion, *pRegion, len);
|
|
(*pRegion)[len] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ures_close(regionsRes);
|
|
ures_close(rbTable);
|
|
|
|
if (regionError) {
|
|
if (names != NULL) {
|
|
uprv_free(names);
|
|
}
|
|
if (regions != NULL) {
|
|
char **p = regions;
|
|
for (int32_t i = 0; i < numRegions; p++, i++) {
|
|
uprv_free(*p);
|
|
}
|
|
uprv_free(regions);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
return new TZDBNames(names, regions, numRegions);
|
|
}
|
|
|
|
const UChar*
|
|
TZDBNames::getName(UTimeZoneNameType type) const {
|
|
if (fNames == NULL) {
|
|
return NULL;
|
|
}
|
|
const UChar *name = NULL;
|
|
switch(type) {
|
|
case UTZNM_SHORT_STANDARD:
|
|
name = fNames[0];
|
|
break;
|
|
case UTZNM_SHORT_DAYLIGHT:
|
|
name = fNames[1];
|
|
break;
|
|
default:
|
|
name = NULL;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
const char**
|
|
TZDBNames::getParseRegions(int32_t& numRegions) const {
|
|
if (fRegions == NULL) {
|
|
numRegions = 0;
|
|
} else {
|
|
numRegions = fNumRegions;
|
|
}
|
|
return (const char**)fRegions;
|
|
}
|
|
|
|
U_CDECL_BEGIN
|
|
/**
|
|
* TZDBNameInfo stores metazone name information for the IANA abbreviations
|
|
* in the trie
|
|
*/
|
|
typedef struct TZDBNameInfo {
|
|
const UChar* mzID;
|
|
UTimeZoneNameType type;
|
|
UBool ambiguousType;
|
|
const char** parseRegions;
|
|
int32_t nRegions;
|
|
} TZDBNameInfo;
|
|
U_CDECL_END
|
|
|
|
|
|
class TZDBNameSearchHandler : public TextTrieMapSearchResultHandler {
|
|
public:
|
|
TZDBNameSearchHandler(uint32_t types, const char* region);
|
|
virtual ~TZDBNameSearchHandler();
|
|
|
|
UBool handleMatch(int32_t matchLength, const CharacterNode *node, UErrorCode &status) override;
|
|
TimeZoneNames::MatchInfoCollection* getMatches(int32_t& maxMatchLen);
|
|
|
|
private:
|
|
uint32_t fTypes;
|
|
int32_t fMaxMatchLen;
|
|
TimeZoneNames::MatchInfoCollection* fResults;
|
|
const char* fRegion;
|
|
};
|
|
|
|
TZDBNameSearchHandler::TZDBNameSearchHandler(uint32_t types, const char* region)
|
|
: fTypes(types), fMaxMatchLen(0), fResults(NULL), fRegion(region) {
|
|
}
|
|
|
|
TZDBNameSearchHandler::~TZDBNameSearchHandler() {
|
|
if (fResults != NULL) {
|
|
delete fResults;
|
|
}
|
|
}
|
|
|
|
UBool
|
|
TZDBNameSearchHandler::handleMatch(int32_t matchLength, const CharacterNode *node, UErrorCode &status) {
|
|
if (U_FAILURE(status)) {
|
|
return false;
|
|
}
|
|
|
|
TZDBNameInfo *match = NULL;
|
|
TZDBNameInfo *defaultRegionMatch = NULL;
|
|
|
|
if (node->hasValues()) {
|
|
int32_t valuesCount = node->countValues();
|
|
for (int32_t i = 0; i < valuesCount; i++) {
|
|
TZDBNameInfo *ninfo = (TZDBNameInfo *)node->getValue(i);
|
|
if (ninfo == NULL) {
|
|
continue;
|
|
}
|
|
if ((ninfo->type & fTypes) != 0) {
|
|
// Some tz database abbreviations are ambiguous. For example,
|
|
// CST means either Central Standard Time or China Standard Time.
|
|
// Unlike CLDR time zone display names, this implementation
|
|
// does not use unique names. And TimeZoneFormat does not expect
|
|
// multiple results returned for the same time zone type.
|
|
// For this reason, this implementation resolve one among same
|
|
// zone type with a same name at this level.
|
|
if (ninfo->parseRegions == NULL) {
|
|
// parseRegions == null means this is the default metazone
|
|
// mapping for the abbreviation.
|
|
if (defaultRegionMatch == NULL) {
|
|
match = defaultRegionMatch = ninfo;
|
|
}
|
|
} else {
|
|
UBool matchRegion = false;
|
|
// non-default metazone mapping for an abbreviation
|
|
// comes with applicable regions. For example, the default
|
|
// metazone mapping for "CST" is America_Central,
|
|
// but if region is one of CN/MO/TW, "CST" is parsed
|
|
// as metazone China (China Standard Time).
|
|
for (int32_t j = 0; j < ninfo->nRegions; j++) {
|
|
const char *region = ninfo->parseRegions[j];
|
|
if (uprv_strcmp(fRegion, region) == 0) {
|
|
match = ninfo;
|
|
matchRegion = true;
|
|
break;
|
|
}
|
|
}
|
|
if (matchRegion) {
|
|
break;
|
|
}
|
|
if (match == NULL) {
|
|
match = ninfo;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (match != NULL) {
|
|
UTimeZoneNameType ntype = match->type;
|
|
// Note: Workaround for duplicated standard/daylight names
|
|
// The tz database contains a few zones sharing a
|
|
// same name for both standard time and daylight saving
|
|
// time. For example, Australia/Sydney observes DST,
|
|
// but "EST" is used for both standard and daylight.
|
|
// When both SHORT_STANDARD and SHORT_DAYLIGHT are included
|
|
// in the find operation, we cannot tell which one was
|
|
// actually matched.
|
|
// TimeZoneFormat#parse returns a matched name type (standard
|
|
// or daylight) and DateFormat implementation uses the info to
|
|
// to adjust actual time. To avoid false type information,
|
|
// this implementation replaces the name type with SHORT_GENERIC.
|
|
if (match->ambiguousType
|
|
&& (ntype == UTZNM_SHORT_STANDARD || ntype == UTZNM_SHORT_DAYLIGHT)
|
|
&& (fTypes & UTZNM_SHORT_STANDARD) != 0
|
|
&& (fTypes & UTZNM_SHORT_DAYLIGHT) != 0) {
|
|
ntype = UTZNM_SHORT_GENERIC;
|
|
}
|
|
|
|
if (fResults == NULL) {
|
|
fResults = new TimeZoneNames::MatchInfoCollection();
|
|
if (fResults == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
}
|
|
}
|
|
if (U_SUCCESS(status)) {
|
|
U_ASSERT(fResults != NULL);
|
|
U_ASSERT(match->mzID != NULL);
|
|
fResults->addMetaZone(ntype, matchLength, UnicodeString(match->mzID, -1), status);
|
|
if (U_SUCCESS(status) && matchLength > fMaxMatchLen) {
|
|
fMaxMatchLen = matchLength;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
TimeZoneNames::MatchInfoCollection*
|
|
TZDBNameSearchHandler::getMatches(int32_t& maxMatchLen) {
|
|
// give the ownership to the caller
|
|
TimeZoneNames::MatchInfoCollection* results = fResults;
|
|
maxMatchLen = fMaxMatchLen;
|
|
|
|
// reset
|
|
fResults = NULL;
|
|
fMaxMatchLen = 0;
|
|
return results;
|
|
}
|
|
|
|
U_CDECL_BEGIN
|
|
/**
|
|
* Deleter for TZDBNames
|
|
*/
|
|
static void U_CALLCONV
|
|
deleteTZDBNames(void *obj) {
|
|
if (obj != EMPTY) {
|
|
delete (TZDBNames *)obj;
|
|
}
|
|
}
|
|
|
|
static void U_CALLCONV initTZDBNamesMap(UErrorCode &status) {
|
|
gTZDBNamesMap = uhash_open(uhash_hashUChars, uhash_compareUChars, NULL, &status);
|
|
if (U_FAILURE(status)) {
|
|
gTZDBNamesMap = NULL;
|
|
return;
|
|
}
|
|
// no key deleters for tzdb name maps
|
|
uhash_setValueDeleter(gTZDBNamesMap, deleteTZDBNames);
|
|
ucln_i18n_registerCleanup(UCLN_I18N_TZDBTIMEZONENAMES, tzdbTimeZoneNames_cleanup);
|
|
}
|
|
|
|
/**
|
|
* Deleter for TZDBNameInfo
|
|
*/
|
|
static void U_CALLCONV
|
|
deleteTZDBNameInfo(void *obj) {
|
|
if (obj != NULL) {
|
|
uprv_free(obj);
|
|
}
|
|
}
|
|
|
|
static void U_CALLCONV prepareFind(UErrorCode &status) {
|
|
if (U_FAILURE(status)) {
|
|
return;
|
|
}
|
|
gTZDBNamesTrie = new TextTrieMap(true, deleteTZDBNameInfo);
|
|
if (gTZDBNamesTrie == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
return;
|
|
}
|
|
|
|
const UnicodeString *mzID;
|
|
StringEnumeration *mzIDs = TimeZoneNamesImpl::_getAvailableMetaZoneIDs(status);
|
|
if (U_SUCCESS(status)) {
|
|
while ((mzID = mzIDs->snext(status)) != 0 && U_SUCCESS(status)) {
|
|
const TZDBNames *names = TZDBTimeZoneNames::getMetaZoneNames(*mzID, status);
|
|
if (U_FAILURE(status)) {
|
|
break;
|
|
}
|
|
if (names == NULL) {
|
|
continue;
|
|
}
|
|
const UChar *std = names->getName(UTZNM_SHORT_STANDARD);
|
|
const UChar *dst = names->getName(UTZNM_SHORT_DAYLIGHT);
|
|
if (std == NULL && dst == NULL) {
|
|
continue;
|
|
}
|
|
int32_t numRegions = 0;
|
|
const char **parseRegions = names->getParseRegions(numRegions);
|
|
|
|
// The tz database contains a few zones sharing a
|
|
// same name for both standard time and daylight saving
|
|
// time. For example, Australia/Sydney observes DST,
|
|
// but "EST" is used for both standard and daylight.
|
|
// we need to store the information for later processing.
|
|
UBool ambiguousType = (std != NULL && dst != NULL && u_strcmp(std, dst) == 0);
|
|
|
|
const UChar *uMzID = ZoneMeta::findMetaZoneID(*mzID);
|
|
if (std != NULL) {
|
|
TZDBNameInfo *stdInf = (TZDBNameInfo *)uprv_malloc(sizeof(TZDBNameInfo));
|
|
if (stdInf == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
break;
|
|
}
|
|
stdInf->mzID = uMzID;
|
|
stdInf->type = UTZNM_SHORT_STANDARD;
|
|
stdInf->ambiguousType = ambiguousType;
|
|
stdInf->parseRegions = parseRegions;
|
|
stdInf->nRegions = numRegions;
|
|
gTZDBNamesTrie->put(std, stdInf, status);
|
|
}
|
|
if (U_SUCCESS(status) && dst != NULL) {
|
|
TZDBNameInfo *dstInf = (TZDBNameInfo *)uprv_malloc(sizeof(TZDBNameInfo));
|
|
if (dstInf == NULL) {
|
|
status = U_MEMORY_ALLOCATION_ERROR;
|
|
break;
|
|
}
|
|
dstInf->mzID = uMzID;
|
|
dstInf->type = UTZNM_SHORT_DAYLIGHT;
|
|
dstInf->ambiguousType = ambiguousType;
|
|
dstInf->parseRegions = parseRegions;
|
|
dstInf->nRegions = numRegions;
|
|
gTZDBNamesTrie->put(dst, dstInf, status);
|
|
}
|
|
}
|
|
}
|
|
delete mzIDs;
|
|
|
|
if (U_FAILURE(status)) {
|
|
delete gTZDBNamesTrie;
|
|
gTZDBNamesTrie = NULL;
|
|
return;
|
|
}
|
|
|
|
ucln_i18n_registerCleanup(UCLN_I18N_TZDBTIMEZONENAMES, tzdbTimeZoneNames_cleanup);
|
|
}
|
|
|
|
U_CDECL_END
|
|
|
|
TZDBTimeZoneNames::TZDBTimeZoneNames(const Locale& locale)
|
|
: fLocale(locale) {
|
|
UBool useWorld = true;
|
|
const char* region = fLocale.getCountry();
|
|
int32_t regionLen = static_cast<int32_t>(uprv_strlen(region));
|
|
if (regionLen == 0) {
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
CharString loc;
|
|
{
|
|
CharStringByteSink sink(&loc);
|
|
ulocimp_addLikelySubtags(fLocale.getName(), sink, &status);
|
|
}
|
|
regionLen = uloc_getCountry(loc.data(), fRegion, sizeof(fRegion), &status);
|
|
if (U_SUCCESS(status) && regionLen < (int32_t)sizeof(fRegion)) {
|
|
useWorld = false;
|
|
}
|
|
} else if (regionLen < (int32_t)sizeof(fRegion)) {
|
|
uprv_strcpy(fRegion, region);
|
|
useWorld = false;
|
|
}
|
|
if (useWorld) {
|
|
uprv_strcpy(fRegion, "001");
|
|
}
|
|
}
|
|
|
|
TZDBTimeZoneNames::~TZDBTimeZoneNames() {
|
|
}
|
|
|
|
bool
|
|
TZDBTimeZoneNames::operator==(const TimeZoneNames& other) const {
|
|
if (this == &other) {
|
|
return true;
|
|
}
|
|
// No implementation for now
|
|
return false;
|
|
}
|
|
|
|
TZDBTimeZoneNames*
|
|
TZDBTimeZoneNames::clone() const {
|
|
return new TZDBTimeZoneNames(fLocale);
|
|
}
|
|
|
|
StringEnumeration*
|
|
TZDBTimeZoneNames::getAvailableMetaZoneIDs(UErrorCode& status) const {
|
|
return TimeZoneNamesImpl::_getAvailableMetaZoneIDs(status);
|
|
}
|
|
|
|
StringEnumeration*
|
|
TZDBTimeZoneNames::getAvailableMetaZoneIDs(const UnicodeString& tzID, UErrorCode& status) const {
|
|
return TimeZoneNamesImpl::_getAvailableMetaZoneIDs(tzID, status);
|
|
}
|
|
|
|
UnicodeString&
|
|
TZDBTimeZoneNames::getMetaZoneID(const UnicodeString& tzID, UDate date, UnicodeString& mzID) const {
|
|
return TimeZoneNamesImpl::_getMetaZoneID(tzID, date, mzID);
|
|
}
|
|
|
|
UnicodeString&
|
|
TZDBTimeZoneNames::getReferenceZoneID(const UnicodeString& mzID, const char* region, UnicodeString& tzID) const {
|
|
return TimeZoneNamesImpl::_getReferenceZoneID(mzID, region, tzID);
|
|
}
|
|
|
|
UnicodeString&
|
|
TZDBTimeZoneNames::getMetaZoneDisplayName(const UnicodeString& mzID,
|
|
UTimeZoneNameType type,
|
|
UnicodeString& name) const {
|
|
name.setToBogus();
|
|
if (mzID.isEmpty()) {
|
|
return name;
|
|
}
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
const TZDBNames *tzdbNames = TZDBTimeZoneNames::getMetaZoneNames(mzID, status);
|
|
if (U_SUCCESS(status)) {
|
|
if (tzdbNames != NULL) {
|
|
const UChar *s = tzdbNames->getName(type);
|
|
if (s != NULL) {
|
|
name.setTo(true, s, -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
UnicodeString&
|
|
TZDBTimeZoneNames::getTimeZoneDisplayName(const UnicodeString& /* tzID */, UTimeZoneNameType /* type */, UnicodeString& name) const {
|
|
// No abbreviations associated a zone directly for now.
|
|
name.setToBogus();
|
|
return name;
|
|
}
|
|
|
|
TZDBTimeZoneNames::MatchInfoCollection*
|
|
TZDBTimeZoneNames::find(const UnicodeString& text, int32_t start, uint32_t types, UErrorCode& status) const {
|
|
umtx_initOnce(gTZDBNamesTrieInitOnce, &prepareFind, status);
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
|
|
TZDBNameSearchHandler handler(types, fRegion);
|
|
gTZDBNamesTrie->search(text, start, (TextTrieMapSearchResultHandler *)&handler, status);
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
int32_t maxLen = 0;
|
|
return handler.getMatches(maxLen);
|
|
}
|
|
|
|
const TZDBNames*
|
|
TZDBTimeZoneNames::getMetaZoneNames(const UnicodeString& mzID, UErrorCode& status) {
|
|
umtx_initOnce(gTZDBNamesMapInitOnce, &initTZDBNamesMap, status);
|
|
if (U_FAILURE(status)) {
|
|
return NULL;
|
|
}
|
|
|
|
TZDBNames* tzdbNames = NULL;
|
|
|
|
UChar mzIDKey[ZID_KEY_MAX + 1];
|
|
mzID.extract(mzIDKey, ZID_KEY_MAX + 1, status);
|
|
U_ASSERT(status == U_ZERO_ERROR); // already checked length above
|
|
mzIDKey[mzID.length()] = 0;
|
|
|
|
static UMutex gTZDBNamesMapLock;
|
|
umtx_lock(&gTZDBNamesMapLock);
|
|
{
|
|
void *cacheVal = uhash_get(gTZDBNamesMap, mzIDKey);
|
|
if (cacheVal == NULL) {
|
|
UResourceBundle *zoneStringsRes = ures_openDirect(U_ICUDATA_ZONE, "tzdbNames", &status);
|
|
zoneStringsRes = ures_getByKey(zoneStringsRes, gZoneStrings, zoneStringsRes, &status);
|
|
if (U_SUCCESS(status)) {
|
|
char key[ZID_KEY_MAX + 1];
|
|
mergeTimeZoneKey(mzID, key);
|
|
tzdbNames = TZDBNames::createInstance(zoneStringsRes, key);
|
|
|
|
if (tzdbNames == NULL) {
|
|
cacheVal = (void *)EMPTY;
|
|
} else {
|
|
cacheVal = tzdbNames;
|
|
}
|
|
// Use the persistent ID as the resource key, so we can
|
|
// avoid duplications.
|
|
// TODO: Is there a more efficient way, like intern() in Java?
|
|
void* newKey = (void*) ZoneMeta::findMetaZoneID(mzID);
|
|
if (newKey != NULL) {
|
|
uhash_put(gTZDBNamesMap, newKey, cacheVal, &status);
|
|
if (U_FAILURE(status)) {
|
|
if (tzdbNames != NULL) {
|
|
delete tzdbNames;
|
|
tzdbNames = NULL;
|
|
}
|
|
}
|
|
} else {
|
|
// Should never happen with a valid input
|
|
if (tzdbNames != NULL) {
|
|
// It's not possible that we get a valid tzdbNames with unknown ID.
|
|
// But just in case..
|
|
delete tzdbNames;
|
|
tzdbNames = NULL;
|
|
}
|
|
}
|
|
}
|
|
ures_close(zoneStringsRes);
|
|
} else if (cacheVal != EMPTY) {
|
|
tzdbNames = (TZDBNames *)cacheVal;
|
|
}
|
|
}
|
|
umtx_unlock(&gTZDBNamesMapLock);
|
|
|
|
return tzdbNames;
|
|
}
|
|
|
|
U_NAMESPACE_END
|
|
|
|
|
|
#endif /* #if !UCONFIG_NO_FORMATTING */
|
|
|
|
//eof
|