From 072d25e8d5728ce2ee62530af20e38b8f879228c Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.com>
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            | 121 +++++++++++++++---
 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, 160 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..b62fa3138e6 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,32 @@ 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) && IS_SUFFIX_TAM(n->suffix))
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_DATETIME_FORMAT),
+								 errmsg("TM and TAM prefixes cannot be used together")));
+
+					if (IS_SUFFIX_TM(n->suffix))
+					{
+						localized_months = localized_full_months;
+						suffix_len = TM_SUFFIX_LEN;
+					}
+					else
+					{
+						/* TAM suffix - localized alternative month name */
+						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 +2837,32 @@ 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) && IS_SUFFIX_TAM(n->suffix))
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_DATETIME_FORMAT),
+								 errmsg("TM and TAM prefixes cannot be used together")));
+
+					if (IS_SUFFIX_TM(n->suffix))
+					{
+						localized_months = localized_full_months;
+						suffix_len = TM_SUFFIX_LEN;
+					}
+					else
+					{
+						/* TAM suffix - localized alternative month name */
+						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 +2878,32 @@ 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) && IS_SUFFIX_TAM(n->suffix))
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_DATETIME_FORMAT),
+								 errmsg("TM and TAM prefixes cannot be used together")));
+
+					if (IS_SUFFIX_TM(n->suffix))
+					{
+						localized_months = localized_full_months;
+						suffix_len = TM_SUFFIX_LEN;
+					}
+					else
+					{
+						/* TAM suffix - localized alternative month name */
+						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 +3638,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

