Hi st 12. 8. 2026 v 23:51 odesílatel Bernd Reiß <[email protected]> napsal:
> Hi Pavel, > > I've looked into your patch and provide the review below. > > Threads I found touching TMMONTH + glibc: > > https://www.postgresql.org/message-id/CALSKcLS3Zi4o0Ak5pNOheqmAiKWcez%2B%2BqwKvyPccvOz2v62QZQ%40mail.gmail.com > (Russian) > https://www.postgresql.org/message-id/14717914.JAiOoc7IO7%40utklippan > > I did not come across a prior thread proposing an implementation of what > this patch does. Worth noting that in the utklippan thread above, Tom Lane > suggested that a new format code or modifier would be the palatable way to > address a similar problem. > > Contents & Purpose > ================== > This patch gives users control over the case (genitive vs. nominative) > to use when pulling localized month names from glibc via the function > to_char(). The author proposes to add a TAMMONTH option for formatting > dates. > > Intended behaviour: TMMONTH uses the genitive form (if applicable to the > language; equivalent to %B) while TAMMONTH uses the nominative > (equivalent to %OB). > > The patch contains regression test cases. It also adds a corresponding > entry in the documentation. > > Initial Run > =========== > The patch applies cleanly to HEAD. The regression tests all pass > successfully against the new patch, but fail against pre-patched HEAD, > so the test cases are sane and do cover the new behavior. > > Manual Testing > ============== > The provided examples all work fine. However, I realized that the > shortened form TAMMON falls back to English instead of honouring > lc_time: > > postgres=# set lc_time to 'de_DE.UTF-8'; > SET > postgres=# SELECT to_char(date '2026-03-01'::date, 'DD TMMON'); > SELECT to_char(date '2026-03-01'::date, 'DD TAMMON'); > to_char > --------- > 01 MÄR > (1 row) > > to_char > --------- > 01 MAR > (1 row) > > It looks like only the full-month cases (DCH_MONTH/Month/month) were > wired up for > TAM; the abbreviated cases (DCH_MON/Mon/mon) still test IS_SUFFIX_TM > only, so TAM > silently drops to the default English abbreviation. I'd expect TAMMON to > stay > localized. > TAMMON is not implemented, because glibc doesn't provide an alternative form for abbreviated month names. It is a question if it is better to raise an error, return a non alternative name or just ignore this flag. I have not strong opinion about this. Inside DCH_to_char the prefix TM is ignored when it is not used. So I did the same. But I can imagine using localized abbreviation. It is possible for the Czech language - but I have not idea if it is true for other languages. > > Code Review > =========== > I am not sure if this condition can be ever true: > > + if (IS_SUFFIX_TM(n->suffix) && IS_SUFFIX_TAM(n->suffix)) > > In case this is not possible the subsequent error message will never be > thrown and the whole block is dead code. > > However, if there is a reason for this check, it would be nice to > have a comment mention it. > Yes, I badly expected that prefixes could be mixed. I checked the code, and this is not possible, so I removed this check > > One thing I am not entirely sure about is this line: > > + if (strftime_l(bufptr, MAX_L10N_DATA, "%OB", timeinfo, locale) <= 0) > + strftimefail = true; > > As far as I checked %OB is not supported on Windows: > > > https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strftime-wcsftime-strftime-l-wcsftime-l?view=msvc-170 > > I was, however, not able to confirm this on a Windows machine right now. > I might be able to do so on the weekend. > > However, the same problem exists with older glibc versions not supporting > %OB. As far as I understand strftime_l this will lead to it returning 0 and > therefore setting strftimefail to true always. Since this if statement > is evaluated > unconditionally I expect this to also influence the behaviour of TM. > I am afraid about this case too, and I expect maybe some fallback mode there. But because I have not a Window machine or some machine with older glibc I decided to don't touch it for this moment. glibc older than 2.28 doesn't support %OB. On the second hand the result of %B is probably equal to expected result %B. > Nitpicking & Conclusion > ======================= > I feel like the documentation could explicitly mention the difference > between TMMONTH and TAMMONTH (i.e., mentioning genitive and nominative > like the Locale docs you mentioned do). I also think it would be > beneficial to > mention that this linguistic detail is specific to certain languages. > please, if you can write this part of the doc. My English is not good enough to write well about these linguistic details. It is a problem primarily for slavic languages - like Czech or Russian - but maybe it can be a wide problem. > Furthermore, in general I feel like the code could have more comments. > The single comment /* TAM suffix - localized alternative month name */ > I would rather put outside the if-else block and set TAM directly in > relation > to TM there. > > All in all, this topic seems to have been a pain point for many people > as the discussion threads mentioned in the beginning attest. The patch > offers a solution to make glibc behaviour more predictable for users. > I see a very real use case here. > Thank you very much for this immediate review Regards Pavel > Best > Bernd > > On 12/08/2026 09:13, Pavel Stehule wrote: > > Hi > > > > út 11. 8. 2026 v 17:14 odesílatel Pavel Stehule > > <[email protected]> napsal: > > > > > > > > út 11. 8. 2026 v 17:06 odesílatel Pavel Stehule > > <[email protected]> napsal: > > > > Hi > > > > My customer reported an interesting issue. He needs translated > > month name, and then he use > > to_char(current_date, 'tmmonth'); > > > > Unfortunately, glibc returns nouns in the genitive case > > instead of the nominative case. > > > > This is a glibc feature from the 2.28 release. Genitive case > > makes sense, when the result holds a day, but without it, it > > is messy. > > > > glibc has alternative month names, that can be taken by usage > > placeholder '%OB' of function strftime. > > > https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap07.html > > - see alt_mon. > > > > Can we enhance prefixes (maybe tmo) to be possible to use > > alternative names? > > > > > > example: > > > > pavel@nemesis:~/src/orafce$ date +'%B' > > srpna > > pavel@nemesis:~/src/orafce$ date +'%OB' > > srpen > > pavel@nemesis:~/src/orafce$ LANG=C date +'%OB' > > August > > > > > > attached patch that implements 'TAM' modifier for data/timestamp > > formatting > > > > Regards > > > > Pavel > > > > > > Regards > > > > Pavel > > > > > > > > > > > >
From c1511bb9165e4b0685a0990632a8ff4ae9d8c1ec Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 12 Aug 2026 07:03:28 +0200 Subject: [PATCH] introduce 'tam' modifier for date/timestamp formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit glibc returns localized month names in the genitive case, but it also provides alternative month names in the nominative case. The TAM prefix allows you to retrieve this alternative name. example: SELECT to_char(date '2026-08-01', 'TMMONTH'); ┌─────────┐ │ to_char │ ╞═════════╡ │ SRPNA │ └─────────┘ (1 row) SELECT to_char(date '2026-08-01', 'TAMMONTH'); ┌─────────┐ │ to_char │ ╞═════════╡ │ SRPEN │ └─────────┘ (1 row) --- doc/src/sgml/func/func-formatting.sgml | 6 + src/backend/utils/adt/formatting.c | 103 +++++++++++++++--- src/backend/utils/adt/pg_locale.c | 9 +- src/include/utils/pg_locale.h | 1 + .../regress/expected/collate.linux.utf8.out | 28 +++++ src/test/regress/sql/collate.linux.utf8.sql | 12 ++ 6 files changed, 142 insertions(+), 17 deletions(-) diff --git a/doc/src/sgml/func/func-formatting.sgml b/doc/src/sgml/func/func-formatting.sgml index e4edaf4f42c..85444015caa 100644 --- a/doc/src/sgml/func/func-formatting.sgml +++ b/doc/src/sgml/func/func-formatting.sgml @@ -477,6 +477,12 @@ <xref linkend="guc-lc-time"/>)</entry> <entry><literal>TMMonth</literal></entry> </row> + <row> + <entry><literal>TAM</literal> prefix</entry> + <entry>translation alternative mode (use localized alternative month names based on + <xref linkend="guc-lc-time"/>)</entry> + <entry><literal>TAMMonth</literal></entry> + </row> <row> <entry><literal>SP</literal> suffix</entry> <entry>spell mode (not implemented)</entry> diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index effad4c37dd..42f7935ac7c 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -553,6 +553,7 @@ do { \ #define DCH_SUFFIX_th 0x04 #define DCH_SUFFIX_SP 0x08 #define DCH_SUFFIX_TM 0x10 +#define DCH_SUFFIX_TAM 0x20 /* * Suffix tests @@ -594,16 +595,25 @@ IS_SUFFIX_TM(uint8 _s) return (_s & DCH_SUFFIX_TM); } +static inline bool +IS_SUFFIX_TAM(uint8 _s) +{ + return (_s & DCH_SUFFIX_TAM); +} + /* * Suffixes definition for DATE-TIME TO/FROM CHAR */ #define TM_SUFFIX_LEN 2 +#define TAM_SUFFIX_LEN 3 static const KeySuffix DCH_suff[] = { {"FM", 2, DCH_SUFFIX_FM, SUFFTYPE_PREFIX}, {"fm", 2, DCH_SUFFIX_FM, SUFFTYPE_PREFIX}, {"TM", TM_SUFFIX_LEN, DCH_SUFFIX_TM, SUFFTYPE_PREFIX}, + {"TAM", TAM_SUFFIX_LEN, DCH_SUFFIX_TAM, SUFFTYPE_PREFIX}, {"tm", 2, DCH_SUFFIX_TM, SUFFTYPE_PREFIX}, + {"tam", 3, DCH_SUFFIX_TAM, SUFFTYPE_PREFIX}, {"TH", 2, DCH_SUFFIX_TH, SUFFTYPE_POSTFIX}, {"th", 2, DCH_SUFFIX_th, SUFFTYPE_POSTFIX}, {"SP", 2, DCH_SUFFIX_SP, SUFFTYPE_POSTFIX}, @@ -2786,11 +2796,26 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (IS_SUFFIX_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix) || IS_SUFFIX_TAM(n->suffix)) { - char *str = str_toupper_z(localized_full_months[tm->tm_mon - 1], collid); + char *str; + char **localized_months; + int suffix_len; - if (strlen(str) <= (n->key->len + TM_SUFFIX_LEN) * DCH_MAX_ITEM_SIZ) + if (IS_SUFFIX_TM(n->suffix)) + { + localized_months = localized_full_months; + suffix_len = TM_SUFFIX_LEN; + } + else + { + localized_months = localized_alt_full_months; + suffix_len = TAM_SUFFIX_LEN; + } + + str = str_toupper_z(localized_months[tm->tm_mon - 1], collid); + + if (strlen(str) <= (n->key->len + suffix_len) * DCH_MAX_ITEM_SIZ) strcpy(s, str); else ereport(ERROR, @@ -2806,11 +2831,26 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (IS_SUFFIX_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix) || IS_SUFFIX_TAM(n->suffix)) { - char *str = str_initcap_z(localized_full_months[tm->tm_mon - 1], collid); + char *str; + char **localized_months; + int suffix_len; - if (strlen(str) <= (n->key->len + TM_SUFFIX_LEN) * DCH_MAX_ITEM_SIZ) + if (IS_SUFFIX_TM(n->suffix)) + { + localized_months = localized_full_months; + suffix_len = TM_SUFFIX_LEN; + } + else + { + localized_months = localized_alt_full_months; + suffix_len = TAM_SUFFIX_LEN; + } + + str = str_initcap_z(localized_months[tm->tm_mon - 1], collid); + + if (strlen(str) <= (n->key->len + suffix_len) * DCH_MAX_ITEM_SIZ) strcpy(s, str); else ereport(ERROR, @@ -2826,11 +2866,26 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (IS_SUFFIX_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix) || IS_SUFFIX_TAM(n->suffix)) { - char *str = str_tolower_z(localized_full_months[tm->tm_mon - 1], collid); + char *str; + char **localized_months; + int suffix_len; - if (strlen(str) <= (n->key->len + TM_SUFFIX_LEN) * DCH_MAX_ITEM_SIZ) + if (IS_SUFFIX_TM(n->suffix)) + { + localized_months = localized_full_months; + suffix_len = TM_SUFFIX_LEN; + } + else + { + localized_months = localized_alt_full_months; + suffix_len = TAM_SUFFIX_LEN; + } + + str = str_tolower_z(localized_months[tm->tm_mon - 1], collid); + + if (strlen(str) <= (n->key->len + suffix_len) * DCH_MAX_ITEM_SIZ) strcpy(s, str); else ereport(ERROR, @@ -3565,13 +3620,29 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, case DCH_MONTH: case DCH_Month: case DCH_month: - if (!from_char_seq_search(&value, &s, months_full, - IS_SUFFIX_TM(n->suffix) ? localized_full_months : NULL, - collid, - n, escontext)) - return; - if (!from_char_set_int(&out->mm, value + 1, n, escontext)) - return; + { + char **months; + + if (IS_SUFFIX_TM(n->suffix) && IS_SUFFIX_TAM(n->suffix)) + ereturn(escontext,, + (errcode(ERRCODE_INVALID_DATETIME_FORMAT), + errmsg("TM and TAM prefixes cannot be used together"))); + + if (IS_SUFFIX_TM(n->suffix)) + months = localized_full_months; + else if (IS_SUFFIX_TAM(n->suffix)) + months = localized_alt_full_months; + else + months = NULL; + + if (!from_char_seq_search(&value, &s, months_full, + months, + collid, + n, escontext)) + return; + if (!from_char_set_int(&out->mm, value + 1, n, escontext)) + return; + } break; case DCH_MON: case DCH_Mon: diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 9eb99487e57..34d758486b5 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -102,6 +102,7 @@ char *localized_abbrev_days[7 + 1]; char *localized_full_days[7 + 1]; char *localized_abbrev_months[12 + 1]; char *localized_full_months[12 + 1]; +char *localized_alt_full_months[12 + 1]; static pg_locale_t default_locale = NULL; @@ -701,7 +702,7 @@ cache_single_string(char **dst, const char *src, int encoding) void cache_locale_time(void) { - char buf[(2 * 7 + 2 * 12) * MAX_L10N_DATA]; + char buf[(2 * 7 + 3 * 12) * MAX_L10N_DATA]; char *bufptr; time_t timenow; struct tm *timeinfo; @@ -768,6 +769,9 @@ cache_locale_time(void) if (strftime_l(bufptr, MAX_L10N_DATA, "%B", timeinfo, locale) <= 0) strftimefail = true; bufptr += MAX_L10N_DATA; + if (strftime_l(bufptr, MAX_L10N_DATA, "%OB", timeinfo, locale) <= 0) + strftimefail = true; + bufptr += MAX_L10N_DATA; } #ifdef WIN32 @@ -824,9 +828,12 @@ cache_locale_time(void) bufptr += MAX_L10N_DATA; cache_single_string(&localized_full_months[i], bufptr, encoding); bufptr += MAX_L10N_DATA; + cache_single_string(&localized_alt_full_months[i], bufptr, encoding); + bufptr += MAX_L10N_DATA; } localized_abbrev_months[12] = NULL; localized_full_months[12] = NULL; + localized_alt_full_months[12] = NULL; CurrentLCTimeValid = true; } diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h index b74821fdfa9..3f79d4f0c1c 100644 --- a/src/include/utils/pg_locale.h +++ b/src/include/utils/pg_locale.h @@ -43,6 +43,7 @@ extern PGDLLIMPORT char *localized_abbrev_days[]; extern PGDLLIMPORT char *localized_full_days[]; extern PGDLLIMPORT char *localized_abbrev_months[]; extern PGDLLIMPORT char *localized_full_months[]; +extern PGDLLIMPORT char *localized_alt_full_months[]; extern bool check_locale(int category, const char *locale, char **canonname); extern char *pg_perm_setlocale(int category, const char *locale); diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out index e0a39e4c300..26a03ac4530 100644 --- a/src/test/regress/expected/collate.linux.utf8.out +++ b/src/test/regress/expected/collate.linux.utf8.out @@ -463,7 +463,22 @@ SELECT to_char(date '2010-04-01', 'DD TMMON YYYY' COLLATE "tr_TR"); 01 NİS 2010 (1 row) +-- to_char +SET lc_time TO 'cs_CZ'; +SELECT to_char(date '2010-02-01', 'DD TMMONTH'); + to_char +---------- + 01 ÚNORA +(1 row) + +SELECT to_char(date '2010-02-01', 'TAMMONTH'); + to_char +--------- + ÚNOR +(1 row) + -- to_date +SET lc_time TO 'tr_TR'; SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY'); to_date ------------ @@ -497,6 +512,19 @@ SELECT to_date('2010 01 araLık', 'YYYY DD TMMONTH'); 12-01-2010 (1 row) +SET lc_time TO 'cs_CZ'; +SELECT to_date('1 srpna 2010', 'DD TMMONTH YYYY'); + to_date +------------ + 08-01-2010 +(1 row) + +SELECT to_date('1 srpen 2010', 'DD TAMMONTH YYYY'); + to_date +------------ + 08-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 6d726ee9c99..3e7c356adb7 100644 --- a/src/test/regress/sql/collate.linux.utf8.sql +++ b/src/test/regress/sql/collate.linux.utf8.sql @@ -182,7 +182,14 @@ SELECT to_char(date '2010-02-01', 'DD TMMON YYYY' COLLATE "tr_TR"); SELECT to_char(date '2010-04-01', 'DD TMMON YYYY'); SELECT to_char(date '2010-04-01', 'DD TMMON YYYY' COLLATE "tr_TR"); +-- to_char +SET lc_time TO 'cs_CZ'; + +SELECT to_char(date '2010-02-01', 'DD TMMONTH'); +SELECT to_char(date '2010-02-01', 'TAMMONTH'); + -- to_date +SET lc_time TO 'tr_TR'; SELECT to_date('01 ŞUB 2010', 'DD TMMON YYYY'); SELECT to_date('01 Şub 2010', 'DD TMMON YYYY'); @@ -192,6 +199,11 @@ 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'); +SET lc_time TO 'cs_CZ'; + +SELECT to_date('1 srpna 2010', 'DD TMMONTH YYYY'); +SELECT to_date('1 srpen 2010', 'DD TAMMONTH YYYY'); + -- backwards parsing CREATE VIEW collview1 AS SELECT * FROM collate_test1 WHERE b COLLATE "C" >= 'bbc'; -- 2.55.0
