diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index fcfe6ed4ba8..54213afb05a 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -114,6 +114,7 @@ #define DCH_MAX_ITEM_SIZ 12 /* max localized day name */ #define NUM_MAX_ITEM_SIZ 8 /* roman number (RN has 15 chars) */ +#define MAX_L10N_DATA 80 /* max localized day or month name */ /* ---------- * Format parser structs @@ -1062,8 +1063,8 @@ static int from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode *node, Node *escontext); static int from_char_parse_int(int *dest, const char **src, FormatNode *node, Node *escontext); -static int seq_search_ascii(const char *name, const char *const *array, int *len); -static int seq_search_localized(const char *name, char **array, int *len, +static int seq_search_ascii(const char *name, const char *const *array, size_t *len); +static int seq_search_localized(const char *name, char **array, size_t *len, Oid collid); static bool from_char_seq_search(int *dest, const char **src, const char *const *array, @@ -2589,7 +2590,7 @@ from_char_parse_int(int *dest, const char **src, FormatNode *node, * suitable for comparisons to ASCII strings. */ static int -seq_search_ascii(const char *name, const char *const *array, int *len) +seq_search_ascii(const char *name, const char *const *array, size_t *len) { unsigned char firstc; const char *const *a; @@ -2634,6 +2635,31 @@ seq_search_ascii(const char *name, const char *const *array, int *len) return -1; } +/* + * Compare 'name' with 'element' in a case-insensitive way, by first + * converting 'name' to upper case, then lower case. ('element' is already + * case-folded that way.) + * + * A helper function for seq_search_localized(). + */ +static bool +casefold_str_cmp(const char *name, size_t name_len, + const char *element, size_t element_len, + Oid collid) +{ + char *upper_substr; + char *lower_substr; + bool result; + + upper_substr = str_toupper(name, name_len, collid); + lower_substr = str_tolower(upper_substr, strlen(upper_substr), collid); + result = strcmp(lower_substr, element) == 0; + + pfree(lower_substr); + pfree(upper_substr); + return result; +} + /* * Sequentially search an array of possibly non-English words for * a case-insensitive match to the initial character(s) of "name". @@ -2646,9 +2672,10 @@ seq_search_ascii(const char *name, const char *const *array, int *len) * the arrays exported by pg_locale.c aren't const. */ static int -seq_search_localized(const char *name, char **array, int *len, Oid collid) +seq_search_localized(const char *name, char **array, size_t *len, Oid collid) { - char **a; + size_t name_len = strlen(name); + const char *name_end = name + name_len; char *upper_name; char *lower_name; @@ -2662,9 +2689,9 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) * The case-folding processing done below is fairly expensive, so before * doing that, make a quick pass to see if there is an exact match. */ - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { - int element_len = strlen(*a); + size_t element_len = strlen(*a); if (strncmp(name, *a, element_len) == 0) { @@ -2677,30 +2704,100 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) * Fold to upper case, then to lower case, so that we can match reliably * even in languages in which case conversions are not injective. */ - upper_name = str_toupper(unconstify(char *, name), strlen(name), collid); + upper_name = str_toupper(name, name_len, collid); lower_name = str_tolower(upper_name, strlen(upper_name), collid); pfree(upper_name); - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { char *upper_element; + size_t upper_element_len; char *lower_element; - int element_len; + size_t lower_element_len; /* Likewise upper/lower-case array element */ upper_element = str_toupper(*a, strlen(*a), collid); - lower_element = str_tolower(upper_element, strlen(upper_element), - collid); + upper_element_len = strlen(upper_element); + lower_element = str_tolower(upper_element, upper_element_len, collid); + lower_element_len = strlen(lower_element); pfree(upper_element); - element_len = strlen(lower_element); - /* Match? */ - if (strncmp(lower_name, lower_element, element_len) == 0) + /* Is 'lower_element' a prefix of 'lower_name' ? */ + if (strncmp(lower_name, lower_element, lower_element_len) == 0) { - *len = element_len; - pfree(lower_element); - pfree(lower_name); - return a - array; + /* + * We have a match, but we still need to figure out how long the + * match is. The case conversions could have changed the lengths + * of either string, or both. + */ + const char *ep; + const char *element_end; + size_t element_nchars; + size_t substr_len; + size_t substr_nchars; + + /* + * First, check the easy case that the string matches as whole. + */ + if (strlen(lower_name) == lower_element_len) + { + *len = name_len; + pfree(lower_element); + pfree(lower_name); + return a - array; + } + + /* + * Another good guess is that the case conversions did not change + * the number of characters. + */ + + /* count characters in the element */ + ep = lower_element; + element_end = lower_element + lower_element_len; + for (element_nchars = 0; ep < element_end; element_nchars++) + ep += pg_mblen_range(ep, element_end); + + /* + * count the byte length of a substring of 'name' having the same + * character count as the element + */ + substr_len = 0; + for (substr_nchars = 0; + substr_nchars < element_nchars && substr_len < name_len; + substr_nchars++) + { + substr_len += pg_mblen_range(name + substr_len, name_end); + } + + if (casefold_str_cmp(name, substr_len, lower_element, lower_element_len, collid)) + { + *len = substr_len; + pfree(lower_element); + pfree(lower_name); + return a - array; + } + + /* + * As last resort, try the case conversion and comparison for + * every substring from the beginning of the original string until + * we find a match. + */ + substr_len = 0; + while (substr_len < name_len) + { + substr_len += pg_mblen_range(name + substr_len, name_end); + + if (casefold_str_cmp(name, substr_len, + lower_element, lower_element_len, + collid)) + { + *len = substr_len; + pfree(lower_element); + pfree(lower_name); + return a - array; + } + } } pfree(lower_element); } @@ -2734,7 +2831,7 @@ from_char_seq_search(int *dest, const char **src, const char *const *array, char **localized_array, Oid collid, FormatNode *node, Node *escontext) { - int len; + size_t len; if (localized_array == NULL) *dest = seq_search_ascii(*src, array, &len); diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out index 01664f7c1b5..e0dc13881c0 100644 --- a/src/test/regress/expected/collate.linux.utf8.out +++ b/src/test/regress/expected/collate.linux.utf8.out @@ -477,6 +477,24 @@ SELECT to_date('01 Şub 2010', 'DD TMMON YYYY'); SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail ERROR: invalid value "1234567890ab" for "MONTH" DETAIL: The given value did not match any of the allowed values for this field. +SELECT to_date('01 Aralık 2010', 'DD TMMONTH YYYY'); + to_date +------------ + 12-01-2010 +(1 row) + +SELECT to_date('01 aralık 2010', 'DD TMMONTH YYYY'); + to_date +------------ + 12-01-2010 +(1 row) + +SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH'); + to_date +------------ + 12-01-2010 +(1 row) + -- backwards parsing CREATE VIEW collview1 AS SELECT * FROM collate_test1 WHERE b COLLATE "C" >= 'bbc'; CREATE VIEW collview2 AS SELECT a, b FROM collate_test1 ORDER BY b COLLATE "C"; diff --git a/src/test/regress/sql/collate.linux.utf8.sql b/src/test/regress/sql/collate.linux.utf8.sql index 132d13af0a8..6d726ee9c99 100644 --- a/src/test/regress/sql/collate.linux.utf8.sql +++ b/src/test/regress/sql/collate.linux.utf8.sql @@ -188,6 +188,9 @@ SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY'); SELECT to_date('01 Şub 2010', 'DD TMMON YYYY'); SELECT to_date('1234567890ab 2010', 'TMMONTH YYYY'); -- fail +SELECT to_date('01 Aralık 2010', 'DD TMMONTH YYYY'); +SELECT to_date('01 aralık 2010', 'DD TMMONTH YYYY'); +SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH'); -- backwards parsing