Support TZ and OF format codes in to_timestamp().
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 25 Jan 2024 22:47:08 +0000 (17:47 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 25 Jan 2024 22:47:08 +0000 (17:47 -0500)
Formerly, these were only supported in to_char(), but there seems
little reason for that restriction.  We should at least have enough
support to permit round-tripping the output of to_char().

In that spirit, TZ accepts either zone abbreviations or numeric
(HH or HH:MM) offsets, which are the cases that to_char() can output.
In an ideal world we'd make it take full zone names too, but
that seems like it'd introduce an unreasonable amount of ambiguity,
since the rules for POSIX-spec zone names are so lax.

OF is a subset of this, accepting only HH or HH:MM.

One small benefit of this improvement is that we can simplify
jsonpath's executeDateTimeMethod function, which no longer needs
to consider the HH and HH:MM cases separately.  Moreover, letting
it accept zone abbreviations means it will accept "Z" to mean UTC,
which is emitted by JSON.stringify() for example.

Patch by me, reviewed by Aleksander Alekseev and Daniel Gustafsson

Discussion: https://postgr.es/m/1681086.1686673242@sss.pgh.pa.us

doc/src/sgml/func.sgml
src/backend/utils/adt/datetime.c
src/backend/utils/adt/formatting.c
src/backend/utils/adt/jsonpath_exec.c
src/include/utils/datetime.h
src/test/regress/expected/horology.out
src/test/regress/expected/jsonb_jsonpath.out
src/test/regress/sql/horology.sql
src/test/regress/sql/jsonb_jsonpath.sql

index 4f7195c5082494f9df1d15049739cd55c60df4e4..6788ba8ef4a5478598930ea6235ea25bb1113630 100644 (file)
@@ -8131,13 +8131,11 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
        </row>
        <row>
         <entry><literal>TZ</literal></entry>
-        <entry>upper case time-zone abbreviation
-         (only supported in <function>to_char</function>)</entry>
+        <entry>upper case time-zone abbreviation</entry>
        </row>
        <row>
         <entry><literal>tz</literal></entry>
-        <entry>lower case time-zone abbreviation
-         (only supported in <function>to_char</function>)</entry>
+        <entry>lower case time-zone abbreviation</entry>
        </row>
        <row>
        <entry><literal>TZH</literal></entry>
@@ -8149,8 +8147,8 @@ SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}');
        </row>
        <row>
         <entry><literal>OF</literal></entry>
-        <entry>time-zone offset from UTC
-         (only supported in <function>to_char</function>)</entry>
+        <entry>time-zone offset from UTC (<replaceable>HH</replaceable>
+         or <replaceable>HH</replaceable><literal>:</literal><replaceable>MM</replaceable>)</entry>
        </row>
       </tbody>
      </tgroup>
index 17b0248bf78a1431892d117d9fbe76b78319b55e..cccabb0c2ad21dc69d8fe72c4a9b13536b54a4c5 100644 (file)
@@ -3246,6 +3246,82 @@ DecodeTimezoneNameToTz(const char *tzname)
        return result;
 }
 
+/* DecodeTimezoneAbbrevPrefix()
+ * Interpret prefix of string as a timezone abbreviation, if possible.
+ *
+ * This has roughly the same functionality as DecodeTimezoneAbbrev(),
+ * but the API is adapted to the needs of formatting.c.  Notably,
+ * we will match the longest possible prefix of the given string
+ * rather than insisting on a complete match, and downcasing is applied
+ * here rather than in the caller.
+ *
+ * Returns the length of the timezone abbreviation, or -1 if not recognized.
+ * On success, sets *offset to the GMT offset for the abbreviation if it
+ * is a fixed-offset abbreviation, or sets *tz to the pg_tz struct for
+ * a dynamic abbreviation.
+ */
+int
+DecodeTimezoneAbbrevPrefix(const char *str, int *offset, pg_tz **tz)
+{
+       char            lowtoken[TOKMAXLEN + 1];
+       int                     len;
+
+       *offset = 0;                            /* avoid uninitialized vars on failure */
+       *tz = NULL;
+
+       if (!zoneabbrevtbl)
+               return -1;                              /* no abbrevs known, so fail immediately */
+
+       /* Downcase as much of the string as we could need */
+       for (len = 0; len < TOKMAXLEN; len++)
+       {
+               if (*str == '\0' || !isalpha((unsigned char) *str))
+                       break;
+               lowtoken[len] = pg_tolower((unsigned char) *str++);
+       }
+       lowtoken[len] = '\0';
+
+       /*
+        * We could avoid doing repeated binary searches if we cared to duplicate
+        * datebsearch here, but it's not clear that such an optimization would be
+        * worth the trouble.  In common cases there's probably not anything after
+        * the zone abbrev anyway.  So just search with successively truncated
+        * strings.
+        */
+       while (len > 0)
+       {
+               const datetkn *tp = datebsearch(lowtoken, zoneabbrevtbl->abbrevs,
+                                                                               zoneabbrevtbl->numabbrevs);
+
+               if (tp != NULL)
+               {
+                       if (tp->type == DYNTZ)
+                       {
+                               DateTimeErrorExtra extra;
+                               pg_tz      *tzp = FetchDynamicTimeZone(zoneabbrevtbl, tp,
+                                                                                                          &extra);
+
+                               if (tzp != NULL)
+                               {
+                                       /* Caller must resolve the abbrev's current meaning */
+                                       *tz = tzp;
+                                       return len;
+                               }
+                       }
+                       else
+                       {
+                               /* Fixed-offset zone abbrev, so it's easy */
+                               *offset = tp->value;
+                               return len;
+                       }
+               }
+               lowtoken[--len] = '\0';
+       }
+
+       /* Did not find a match */
+       return -1;
+}
+
 
 /* ClearPgItmIn
  *
index 83e1f1265ce515d661d12933734f9917874c5157..829aaa8d0e7fcbc804cb764442dbde8c50b327d1 100644 (file)
@@ -418,14 +418,24 @@ typedef struct
                                us,
                                yysz,                   /* is it YY or YYYY ? */
                                clock,                  /* 12 or 24 hour clock? */
-                               tzsign,                 /* +1, -1 or 0 if timezone info is absent */
+                               tzsign,                 /* +1, -1, or 0 if no TZH/TZM fields */
                                tzh,
                                tzm,
                                ff;                             /* fractional precision */
+       bool            has_tz;                 /* was there a TZ field? */
+       int                     gmtoffset;              /* GMT offset of fixed-offset zone abbrev */
+       pg_tz      *tzp;                        /* pg_tz for dynamic abbrev */
+       char       *abbrev;                     /* dynamic abbrev */
 } TmFromChar;
 
 #define ZERO_tmfc(_X) memset(_X, 0, sizeof(TmFromChar))
 
+struct fmt_tz                                  /* do_to_timestamp's timezone info output */
+{
+       bool            has_tz;                 /* was there any TZ/TZH/TZM field? */
+       int                     gmtoffset;              /* GMT offset in seconds */
+};
+
 /* ----------
  * Debug
  * ----------
@@ -1058,8 +1068,8 @@ static bool from_char_seq_search(int *dest, const char **src,
                                                                 char **localized_array, Oid collid,
                                                                 FormatNode *node, Node *escontext);
 static bool do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
-                                                       struct pg_tm *tm, fsec_t *fsec, int *fprec,
-                                                       uint32 *flags, Node *escontext);
+                                                       struct pg_tm *tm, fsec_t *fsec, struct fmt_tz *tz,
+                                                       int *fprec, uint32 *flags, Node *escontext);
 static char *fill_str(char *str, int c, int max);
 static FormatNode *NUM_cache(int len, NUMDesc *Num, text *pars_str, bool *shouldFree);
 static char *int_to_roman(int number);
@@ -3444,7 +3454,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out,
                        case DCH_FF5:
                        case DCH_FF6:
                                out->ff = n->key->id - DCH_FF1 + 1;
-                               /* fall through */
+                               /* FALLTHROUGH */
                        case DCH_US:            /* microsecond */
                                len = from_char_parse_int_len(&out->us, &s,
                                                                                          n->key->id == DCH_US ? 6 :
@@ -3467,11 +3477,63 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out,
                                break;
                        case DCH_tz:
                        case DCH_TZ:
+                               {
+                                       int                     tzlen;
+
+                                       tzlen = DecodeTimezoneAbbrevPrefix(s,
+                                                                                                          &out->gmtoffset,
+                                                                                                          &out->tzp);
+                                       if (tzlen > 0)
+                                       {
+                                               out->has_tz = true;
+                                               /* we only need the zone abbrev for DYNTZ case */
+                                               if (out->tzp)
+                                                       out->abbrev = pnstrdup(s, tzlen);
+                                               out->tzsign = 0;        /* drop any earlier TZH/TZM info */
+                                               s += tzlen;
+                                               break;
+                                       }
+                                       else if (isalpha((unsigned char) *s))
+                                       {
+                                               /*
+                                                * It doesn't match any abbreviation, but it starts
+                                                * with a letter.  OF format certainly won't succeed;
+                                                * assume it's a misspelled abbreviation and complain
+                                                * accordingly.
+                                                */
+                                               ereturn(escontext,,
+                                                               (errcode(ERRCODE_INVALID_DATETIME_FORMAT),
+                                                                errmsg("invalid value \"%s\" for \"%s\"",
+                                                                               s, n->key->name),
+                                                                errdetail("Time zone abbreviation is not recognized.")));
+                                       }
+                                       /* otherwise parse it like OF */
+                               }
+                               /* FALLTHROUGH */
                        case DCH_OF:
-                               ereturn(escontext,,
-                                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                                                errmsg("formatting field \"%s\" is only supported in to_char",
-                                                               n->key->name)));
+                               /* OF is equivalent to TZH or TZH:TZM */
+                               /* see TZH comments below */
+                               if (*s == '+' || *s == '-' || *s == ' ')
+                               {
+                                       out->tzsign = *s == '-' ? -1 : +1;
+                                       s++;
+                               }
+                               else
+                               {
+                                       if (extra_skip > 0 && *(s - 1) == '-')
+                                               out->tzsign = -1;
+                                       else
+                                               out->tzsign = +1;
+                               }
+                               if (from_char_parse_int_len(&out->tzh, &s, 2, n, escontext) < 0)
+                                       return;
+                               if (*s == ':')
+                               {
+                                       s++;
+                                       if (from_char_parse_int_len(&out->tzm, &s, 2, n,
+                                                                                               escontext) < 0)
+                                               return;
+                               }
                                break;
                        case DCH_TZH:
 
@@ -4167,22 +4229,16 @@ to_timestamp(PG_FUNCTION_ARGS)
        Timestamp       result;
        int                     tz;
        struct pg_tm tm;
+       struct fmt_tz ftz;
        fsec_t          fsec;
        int                     fprec;
 
        do_to_timestamp(date_txt, fmt, collid, false,
-                                       &tm, &fsec, &fprec, NULL, NULL);
+                                       &tm, &fsec, &ftz, &fprec, NULL, NULL);
 
        /* Use the specified time zone, if any. */
-       if (tm.tm_zone)
-       {
-               DateTimeErrorExtra extra;
-               int                     dterr = DecodeTimezone(tm.tm_zone, &tz);
-
-               if (dterr)
-                       DateTimeParseError(dterr, &extra, text_to_cstring(date_txt),
-                                                          "timestamptz", NULL);
-       }
+       if (ftz.has_tz)
+               tz = ftz.gmtoffset;
        else
                tz = DetermineTimeZoneOffset(&tm, session_timezone);
 
@@ -4211,10 +4267,11 @@ to_date(PG_FUNCTION_ARGS)
        Oid                     collid = PG_GET_COLLATION();
        DateADT         result;
        struct pg_tm tm;
+       struct fmt_tz ftz;
        fsec_t          fsec;
 
        do_to_timestamp(date_txt, fmt, collid, false,
-                                       &tm, &fsec, NULL, NULL, NULL);
+                                       &tm, &fsec, &ftz, NULL, NULL, NULL);
 
        /* Prevent overflow in Julian-day routines */
        if (!IS_VALID_JULIAN(tm.tm_year, tm.tm_mon, tm.tm_mday))
@@ -4256,12 +4313,13 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
                           Node *escontext)
 {
        struct pg_tm tm;
+       struct fmt_tz ftz;
        fsec_t          fsec;
        int                     fprec;
        uint32          flags;
 
        if (!do_to_timestamp(date_txt, fmt, collid, strict,
-                                                &tm, &fsec, &fprec, &flags, escontext))
+                                                &tm, &fsec, &ftz, &fprec, &flags, escontext))
                return (Datum) 0;
 
        *typmod = fprec ? fprec : -1;   /* fractional part precision */
@@ -4274,18 +4332,9 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
                        {
                                TimestampTz result;
 
-                               if (tm.tm_zone)
+                               if (ftz.has_tz)
                                {
-                                       DateTimeErrorExtra extra;
-                                       int                     dterr = DecodeTimezone(tm.tm_zone, tz);
-
-                                       if (dterr)
-                                       {
-                                               DateTimeParseError(dterr, &extra,
-                                                                                  text_to_cstring(date_txt),
-                                                                                  "timestamptz", escontext);
-                                               return (Datum) 0;
-                                       }
+                                       *tz = ftz.gmtoffset;
                                }
                                else
                                {
@@ -4366,18 +4415,9 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
                {
                        TimeTzADT  *result = palloc(sizeof(TimeTzADT));
 
-                       if (tm.tm_zone)
+                       if (ftz.has_tz)
                        {
-                               DateTimeErrorExtra extra;
-                               int                     dterr = DecodeTimezone(tm.tm_zone, tz);
-
-                               if (dterr)
-                               {
-                                       DateTimeParseError(dterr, &extra,
-                                                                          text_to_cstring(date_txt),
-                                                                          "timetz", escontext);
-                                       return (Datum) 0;
-                               }
+                               *tz = ftz.gmtoffset;
                        }
                        else
                        {
@@ -4430,7 +4470,7 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
  * do_to_timestamp: shared code for to_timestamp and to_date
  *
  * Parse the 'date_txt' according to 'fmt', return results as a struct pg_tm,
- * fractional seconds, and fractional precision.
+ * fractional seconds, struct fmt_tz, and fractional precision.
  *
  * 'collid' identifies the collation to use, if needed.
  * 'std' specifies standard parsing mode.
@@ -4447,12 +4487,12 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
  * 'date_txt'.
  *
  * The TmFromChar is then analysed and converted into the final results in
- * struct 'tm', 'fsec', and 'fprec'.
+ * struct 'tm', 'fsec', struct 'tz', and 'fprec'.
  */
 static bool
 do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
-                               struct pg_tm *tm, fsec_t *fsec, int *fprec,
-                               uint32 *flags, Node *escontext)
+                               struct pg_tm *tm, fsec_t *fsec, struct fmt_tz *tz,
+                               int *fprec, uint32 *flags, Node *escontext)
 {
        FormatNode *format = NULL;
        TmFromChar      tmfc;
@@ -4469,6 +4509,7 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
        ZERO_tmfc(&tmfc);
        ZERO_tm(tm);
        *fsec = 0;
+       tz->has_tz = false;
        if (fprec)
                *fprec = 0;
        if (flags)
@@ -4744,11 +4785,14 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
                goto fail;
        }
 
-       /* Save parsed time-zone into tm->tm_zone if it was specified */
+       /*
+        * If timezone info was present, reduce it to a GMT offset.  (We cannot do
+        * this until we've filled all of the tm struct, since the zone's offset
+        * might be time-varying.)
+        */
        if (tmfc.tzsign)
        {
-               char       *tz;
-
+               /* TZH and/or TZM fields */
                if (tmfc.tzh < 0 || tmfc.tzh > MAX_TZDISP_HOUR ||
                        tmfc.tzm < 0 || tmfc.tzm >= MINS_PER_HOUR)
                {
@@ -4757,10 +4801,27 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std,
                        goto fail;
                }
 
-               tz = psprintf("%c%02d:%02d",
-                                         tmfc.tzsign > 0 ? '+' : '-', tmfc.tzh, tmfc.tzm);
-
-               tm->tm_zone = tz;
+               tz->has_tz = true;
+               tz->gmtoffset = (tmfc.tzh * MINS_PER_HOUR + tmfc.tzm) * SECS_PER_MINUTE;
+               /* note we are flipping the sign convention here */
+               if (tmfc.tzsign > 0)
+                       tz->gmtoffset = -tz->gmtoffset;
+       }
+       else if (tmfc.has_tz)
+       {
+               /* TZ field */
+               tz->has_tz = true;
+               if (tmfc.tzp == NULL)
+               {
+                       /* fixed-offset abbreviation; flip the sign convention */
+                       tz->gmtoffset = -tmfc.gmtoffset;
+               }
+               else
+               {
+                       /* dynamic-offset abbreviation, resolve using specified time */
+                       tz->gmtoffset = DetermineTimeZoneAbbrevOffset(tm, tmfc.abbrev,
+                                                                                                                 tmfc.tzp);
+               }
        }
 
        DEBUG_TM(tm);
index 02a700292bbc036450af2db0370940466d520dc7..22f598cd3592c596d5bbb39e29b5da920c6751d3 100644 (file)
@@ -2296,20 +2296,14 @@ executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp,
                static const char *fmt_str[] =
                {
                        "yyyy-mm-dd",           /* date */
-                       "HH24:MI:SS.USTZH:TZM", /* timetz */
-                       "HH24:MI:SS.USTZH",
-                       "HH24:MI:SSTZH:TZM",
-                       "HH24:MI:SSTZH",
+                       "HH24:MI:SS.USTZ",      /* timetz */
+                       "HH24:MI:SSTZ",
                        "HH24:MI:SS.US",        /* time without tz */
                        "HH24:MI:SS",
-                       "yyyy-mm-dd HH24:MI:SS.USTZH:TZM",      /* timestamptz */
-                       "yyyy-mm-dd HH24:MI:SS.USTZH",
-                       "yyyy-mm-dd HH24:MI:SSTZH:TZM",
-                       "yyyy-mm-dd HH24:MI:SSTZH",
-                       "yyyy-mm-dd\"T\"HH24:MI:SS.USTZH:TZM",
-                       "yyyy-mm-dd\"T\"HH24:MI:SS.USTZH",
-                       "yyyy-mm-dd\"T\"HH24:MI:SSTZH:TZM",
-                       "yyyy-mm-dd\"T\"HH24:MI:SSTZH",
+                       "yyyy-mm-dd HH24:MI:SS.USTZ",   /* timestamptz */
+                       "yyyy-mm-dd HH24:MI:SSTZ",
+                       "yyyy-mm-dd\"T\"HH24:MI:SS.USTZ",
+                       "yyyy-mm-dd\"T\"HH24:MI:SSTZ",
                        "yyyy-mm-dd HH24:MI:SS.US", /* timestamp without tz */
                        "yyyy-mm-dd HH24:MI:SS",
                        "yyyy-mm-dd\"T\"HH24:MI:SS.US",
index 460c75cfdd085d426e779ae7301f8eaa085ceea7..e4ac2b8e7f6c9b5551e7779470cde77c6da3e665 100644 (file)
@@ -348,6 +348,9 @@ extern int  DecodeUnits(int field, const char *lowtoken, int *val);
 extern int     DecodeTimezoneName(const char *tzname, int *offset, pg_tz **tz);
 extern pg_tz *DecodeTimezoneNameToTz(const char *tzname);
 
+extern int     DecodeTimezoneAbbrevPrefix(const char *str,
+                                                                          int *offset, pg_tz **tz);
+
 extern int     j2day(int date);
 
 extern struct Node *TemporalSimplify(int32 max_precis, struct Node *node);
index cfb4b205e4d4fc8f0648177b194dcf41602cc2ef..0bdb76695a19aabef6ebc4525cdfafa679fe574c 100644 (file)
@@ -3140,8 +3140,66 @@ SELECT to_timestamp('2011-12-18 11:38 20',     'YYYY-MM-DD HH12:MI TZM');
  Sun Dec 18 03:18:00 2011 PST
 (1 row)
 
-SELECT to_timestamp('2011-12-18 11:38 PST', 'YYYY-MM-DD HH12:MI TZ');  -- NYI
-ERROR:  formatting field "TZ" is only supported in to_char
+SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 08:38:00 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI TZ');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 08:38:00 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI TZ');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 02:08:00 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
+         to_timestamp         
+------------------------------
+ Sat Dec 17 23:38:00 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 08:38:24 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38-05FOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 08:38:24 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38 JUNK', 'YYYY-MM-DD HH12:MI TZ');  -- error
+ERROR:  invalid value "JUNK" for "TZ"
+DETAIL:  Time zone abbreviation is not recognized.
+SELECT to_timestamp('2011-12-18 11:38 ...', 'YYYY-MM-DD HH12:MI TZ');  -- error
+ERROR:  invalid value ".." for "TZ"
+DETAIL:  Value must be an integer.
+SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI OF');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 08:38:00 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI OF');
+         to_timestamp         
+------------------------------
+ Sun Dec 18 02:08:00 2011 PST
+(1 row)
+
+SELECT to_timestamp('2011-12-18 11:38 +xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+ERROR:  invalid value "xy" for "OF"
+DETAIL:  Value must be an integer.
+SELECT to_timestamp('2011-12-18 11:38 +01:xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+ERROR:  invalid value "xy" for "OF"
+DETAIL:  Value must be an integer.
 SELECT to_timestamp('2018-11-02 12:34:56.025', 'YYYY-MM-DD HH24:MI:SS.MS');
            to_timestamp           
 ----------------------------------
@@ -3557,6 +3615,19 @@ SELECT to_date('0000-02-01','YYYY-MM-DD');  -- allowed, though it shouldn't be
  02-01-0001 BC
 (1 row)
 
+-- to_char's TZ format code produces zone abbrev if known
+SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+         to_char         
+-------------------------
+ 2012-12-12 12:00:00 PST
+(1 row)
+
+SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS tz');
+         to_char         
+-------------------------
+ 2012-12-12 12:00:00 pst
+(1 row)
+
 --
 -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572)
 --
@@ -3598,4 +3669,11 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSSS');
  2012-12-12 43200
 (1 row)
 
+SET TIME ZONE '+2';
+SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+         to_char         
+-------------------------
+ 2012-12-12 12:00:00 +02
+(1 row)
+
 RESET TIME ZONE;
index e758d729f435187ee95cfaeb34a5b6011b27d36a..eea2af30c8b2c3568545a95f8a8b4825c6e887f5 100644 (file)
@@ -3228,6 +3228,18 @@ select jsonb_path_query('"2017-03-10T12:34:56.789+3:10"', '$.datetime()');
 select jsonb_path_query('"2017-03-10t12:34:56.789+3:10"', '$.datetime()');
 ERROR:  datetime format is not recognized: "2017-03-10t12:34:56.789+3:10"
 HINT:  Use a datetime template argument to specify the input data format.
+select jsonb_path_query('"2017-03-10T12:34:56.789EST"', '$.datetime()');
+        jsonb_path_query         
+---------------------------------
+ "2017-03-10T12:34:56.789-05:00"
+(1 row)
+
+select jsonb_path_query('"2017-03-10T12:34:56.789Z"', '$.datetime()');
+        jsonb_path_query         
+---------------------------------
+ "2017-03-10T12:34:56.789+00:00"
+(1 row)
+
 select jsonb_path_query('"12:34:56"', '$.datetime().type()');
      jsonb_path_query     
 --------------------------
index 252bce4b1ce186863de22fb7d6c0c7d5b2e24e8e..3b9573e954c84053696ebc0ed2bf2dc9442700f2 100644 (file)
@@ -501,7 +501,19 @@ SELECT to_timestamp('2011-12-18 11:38 +05:20', 'YYYY-MM-DD HH12:MI TZH:TZM');
 SELECT to_timestamp('2011-12-18 11:38 -05:20', 'YYYY-MM-DD HH12:MI TZH:TZM');
 SELECT to_timestamp('2011-12-18 11:38 20',     'YYYY-MM-DD HH12:MI TZM');
 
-SELECT to_timestamp('2011-12-18 11:38 PST', 'YYYY-MM-DD HH12:MI TZ');  -- NYI
+SELECT to_timestamp('2011-12-18 11:38 EST', 'YYYY-MM-DD HH12:MI TZ');
+SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI TZ');
+SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI TZ');
+SELECT to_timestamp('2011-12-18 11:38 MSK', 'YYYY-MM-DD HH12:MI TZ');  -- dyntz
+SELECT to_timestamp('2011-12-18 11:38ESTFOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
+SELECT to_timestamp('2011-12-18 11:38-05FOO24', 'YYYY-MM-DD HH12:MITZFOOSS');
+SELECT to_timestamp('2011-12-18 11:38 JUNK', 'YYYY-MM-DD HH12:MI TZ');  -- error
+SELECT to_timestamp('2011-12-18 11:38 ...', 'YYYY-MM-DD HH12:MI TZ');  -- error
+
+SELECT to_timestamp('2011-12-18 11:38 -05', 'YYYY-MM-DD HH12:MI OF');
+SELECT to_timestamp('2011-12-18 11:38 +01:30', 'YYYY-MM-DD HH12:MI OF');
+SELECT to_timestamp('2011-12-18 11:38 +xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
+SELECT to_timestamp('2011-12-18 11:38 +01:xyz', 'YYYY-MM-DD HH12:MI OF');  -- error
 
 SELECT to_timestamp('2018-11-02 12:34:56.025', 'YYYY-MM-DD HH24:MI:SS.MS');
 
@@ -616,6 +628,10 @@ SELECT to_date('2016 366', 'YYYY DDD');  -- ok
 SELECT to_date('2016 367', 'YYYY DDD');
 SELECT to_date('0000-02-01','YYYY-MM-DD');  -- allowed, though it shouldn't be
 
+-- to_char's TZ format code produces zone abbrev if known
+SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS tz');
+
 --
 -- Check behavior with SQL-style fixed-GMT-offset time zone (cf bug #8572)
 --
@@ -632,4 +648,8 @@ SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSS');
 SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD SSSSS');
 
+SET TIME ZONE '+2';
+
+SELECT to_char('2012-12-12 12:00'::timestamptz, 'YYYY-MM-DD HH:MI:SS TZ');
+
 RESET TIME ZONE;
index 418eeac5ec716e48a3388f90ba9224b31aae64b0..6406a2ea3b0dc15097e8f271d3af67859e947560 100644 (file)
@@ -755,6 +755,8 @@ select jsonb_path_query('"2017-03-10t12:34:56+3:10"', '$.datetime()');
 select jsonb_path_query('"2017-03-10 12:34:56.789+3:10"', '$.datetime()');
 select jsonb_path_query('"2017-03-10T12:34:56.789+3:10"', '$.datetime()');
 select jsonb_path_query('"2017-03-10t12:34:56.789+3:10"', '$.datetime()');
+select jsonb_path_query('"2017-03-10T12:34:56.789EST"', '$.datetime()');
+select jsonb_path_query('"2017-03-10T12:34:56.789Z"', '$.datetime()');
 select jsonb_path_query('"12:34:56"', '$.datetime().type()');
 select jsonb_path_query('"12:34:56"', '$.datetime()');
 select jsonb_path_query('"12:34:56+3"', '$.datetime().type()');