diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 4956764..6fee7df 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -2955,6 +2955,13 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) value; bool fx_mode = false; +#define ERROR_0YEAR(code_node1,txt_node1,txt_node2) \ + ereport(ERROR, \ + (errcode(ERRCODE_INVALID_DATETIME_FORMAT), \ + errmsg("invalid input string for \"%s\"", \ + n->key->id == (code_node1) ? (txt_node1) : (txt_node2)), \ + errdetail("Year cannot be 0."))); + for (n = node, s = in; n->type != NODE_TYPE_END && *s != '\0'; n++) { if (n->type != NODE_TYPE_ACTION) @@ -3145,6 +3152,11 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) break; case DCH_CC: from_char_parse_int(&out->cc, &s, n); + if (out->cc == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_DATETIME_FORMAT), + errmsg("invalid input string for \"CC\""), + errdetail("Century cannot be 0."))); s += SKIP_THth(n->suffix); break; case DCH_Y_YYY: @@ -3154,19 +3166,30 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) millenia; matched = sscanf(s, "%d,%03d", &millenia, &years); - if (matched != 2) + if ((matched != 2) || (years < 0)) ereport(ERROR, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("invalid input string for \"Y,YYY\""))); - years += (millenia * 1000); + years += (abs(millenia) * 1000); + /* Handle case '-0,001' (parsed as millenia=0, years=1) */ + if (*s == '-') + years = -years; from_char_set_int(&out->year, years, n); + if (out->year == 0) + ERROR_0YEAR(DCH_Y_YYY, "Y,YYY", NULL); out->yysz = 4; - s += strdigits_len(s) + 4 + SKIP_THth(n->suffix); + if (*s == '-') + /* "s + 1" is to skip leading sign*/ + s += strdigits_len(s + 1) + 4 + SKIP_THth(n->suffix); + else + s += strdigits_len(s) + 4 + SKIP_THth(n->suffix); } break; case DCH_YYYY: case DCH_IYYY: from_char_parse_int(&out->year, &s, n); + if (out->year == 0) + ERROR_0YEAR(DCH_YYYY, "YYYY", "IYYY"); out->yysz = 4; s += SKIP_THth(n->suffix); break; @@ -3174,6 +3197,8 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) case DCH_IYY: if (from_char_parse_int(&out->year, &s, n) < 4) out->year = adjust_partial_year_to_2020(out->year); + if (out->year == 0) + ERROR_0YEAR(DCH_YYY, "YYY", "IYY"); out->yysz = 3; s += SKIP_THth(n->suffix); break; @@ -3181,6 +3206,8 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) case DCH_IY: if (from_char_parse_int(&out->year, &s, n) < 4) out->year = adjust_partial_year_to_2020(out->year); + if (out->year == 0) + ERROR_0YEAR(DCH_YY, "YY", "IY"); out->yysz = 2; s += SKIP_THth(n->suffix); break; @@ -3188,6 +3215,8 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) case DCH_I: if (from_char_parse_int(&out->year, &s, n) < 4) out->year = adjust_partial_year_to_2020(out->year); + if (out->year == 0) + ERROR_0YEAR(DCH_Y, "Y", "I"); out->yysz = 1; s += SKIP_THth(n->suffix); break; @@ -3211,6 +3240,7 @@ DCH_from_char(FormatNode *node, char *in, TmFromChar *out) break; } } +#undef ERROR_0YEAR } static DCHCacheEntry * @@ -3680,7 +3710,7 @@ do_to_timestamp(text *date_txt, text *fmt, */ if (tmfc.cc && tmfc.yysz <= 2) { - if (tmfc.bc) + if (tmfc.bc && (tmfc.cc > 0)) tmfc.cc = -tmfc.cc; tm->tm_year = tmfc.year % 100; if (tm->tm_year) @@ -3698,13 +3728,17 @@ do_to_timestamp(text *date_txt, text *fmt, /* If a 4-digit year is provided, we use that and ignore CC. */ { tm->tm_year = tmfc.year; - if (tmfc.bc && tm->tm_year > 0) + if (tm->tm_year < 0) + /* "BC" is ignored if year is already negative */ + tm->tm_year = tm->tm_year + 1; + else if (tmfc.bc) + /* if year is positive, check for BC era */ tm->tm_year = -(tm->tm_year - 1); } } else if (tmfc.cc) /* use first year of century */ { - if (tmfc.bc) + if (tmfc.bc && (tmfc.cc > 0)) tmfc.cc = -tmfc.cc; if (tmfc.cc >= 0) /* +1 because 21st century started in 2001 */ diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out index 1fe02be..193f207 100644 --- a/src/test/regress/expected/horology.out +++ b/src/test/regress/expected/horology.out @@ -2982,6 +2982,131 @@ SELECT to_date('2011 12 18', 'YYYY MM DD'); (1 row) -- +-- Check for negative years +-- +SELECT to_date( '2,008 06 30', 'Y,YYY MM DD'); + to_date +------------ + 06-30-2008 +(1 row) + +SELECT to_date( '0,001 06 30', 'Y,YYY MM DD'); + to_date +------------ + 06-30-0001 +(1 row) + +SELECT to_date( '0,000 06 30', 'Y,YYY MM DD'); -- should fail: there is no "0" year +ERROR: invalid input string for "Y,YYY" +DETAIL: Year cannot be 0. +SELECT to_date( '0,-01 06 30', 'Y,YYY MM DD'); -- should fail: "invalid input string" +ERROR: invalid input string for "Y,YYY" +SELECT to_date('-0,001 06 30', 'Y,YYY MM DD'); + to_date +--------------- + 06-30-0001 BC +(1 row) + +SELECT to_date('-2,008 06 30', 'Y,YYY MM DD'); + to_date +--------------- + 06-30-2008 BC +(1 row) + +SELECT to_date('-2,008 06 30 BC', 'Y,YYY MM DD BC'); -- "BC" is ignored when year is negative + to_date +--------------- + 06-30-2008 BC +(1 row) + +SELECT to_date( '0001 06 30', 'YYYY MM DD'); + to_date +------------ + 06-30-0001 +(1 row) + +SELECT to_date( '0000 06 30', 'YYYY MM DD'); -- should fail: there is no "0" year +ERROR: invalid input string for "YYYY" +DETAIL: Year cannot be 0. +SELECT to_date('-0001 06 30', 'YYYY MM DD'); + to_date +--------------- + 06-30-0001 BC +(1 row) + +SELECT to_date( '000 06 30', 'YYY MM DD'); -- OK, '2000-06-30' + to_date +------------ + 06-30-2000 +(1 row) + +SELECT to_date('-000 06 30', 'YYY MM DD'); -- should fail, fallbacks to "YYYY", 0 year +ERROR: invalid input string for "YYY" +DETAIL: Year cannot be 0. +SELECT to_date('-001 06 30', 'YYY MM DD'); -- OK, fallbacks to "YYYY", 1BC year + to_date +--------------- + 06-30-0001 BC +(1 row) + +SELECT to_date( '-01 06 30', 'YYY MM DD'); + to_date +------------ + 06-30-1999 +(1 row) + +SELECT to_date( '-1 06 30 BC', 'CC MM DD BC'); -- "BC" is ignored when year is negative + to_date +--------------- + 06-30-0100 BC +(1 row) + +SELECT to_date( '-1 06 30 AD', 'CC MM DD BC'); + to_date +--------------- + 06-30-0100 BC +(1 row) + +SELECT to_date( '-0001 06 30 BC', 'YYYY MM DD BC'); -- "BC" is ignored when year is negative + to_date +--------------- + 06-30-0001 BC +(1 row) + +SELECT to_date( '-0001 06 30 AD', 'YYYY MM DD BC'); + to_date +--------------- + 06-30-0001 BC +(1 row) + +SELECT to_date('3220 184', 'IYYY IDDD'); + to_date +------------ + 06-30-3220 +(1 row) + +SELECT to_date('3220 184 BC', 'IYYY IDDD BC'); + to_date +--------------- + 06-30-3220 BC +(1 row) + +SELECT to_date('-3220 184', 'IYYY IDDD'); + to_date +--------------- + 06-30-3220 BC +(1 row) + +SELECT to_date('-3220 184 BC', 'IYYY IDDD BC'); -- "BC" is ignored when year is negative + to_date +--------------- + 06-30-3220 BC +(1 row) + +SELECT to_date('0000 184', 'IYYY IDDD'); -- check for errormsg ("IYYY" in detail) +ERROR: invalid input string for "IYYY" +DETAIL: Year cannot be 0. +-- -- Check errors for some incorrect usages of to_timestamp() -- -- Mixture of date conventions (ISO week and Gregorian): diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql index c81437b..6d54fee 100644 --- a/src/test/regress/sql/horology.sql +++ b/src/test/regress/sql/horology.sql @@ -461,6 +461,39 @@ SELECT to_date('2011 12 18', 'YYYY MM DD'); SELECT to_date('2011 12 18', 'YYYY MM DD'); -- +-- Check for negative years +-- +SELECT to_date( '2,008 06 30', 'Y,YYY MM DD'); +SELECT to_date( '0,001 06 30', 'Y,YYY MM DD'); +SELECT to_date( '0,000 06 30', 'Y,YYY MM DD'); -- should fail: there is no "0" year +SELECT to_date( '0,-01 06 30', 'Y,YYY MM DD'); -- should fail: "invalid input string" +SELECT to_date('-0,001 06 30', 'Y,YYY MM DD'); +SELECT to_date('-2,008 06 30', 'Y,YYY MM DD'); +SELECT to_date('-2,008 06 30 BC', 'Y,YYY MM DD BC'); -- "BC" is ignored when year is negative + +SELECT to_date( '0001 06 30', 'YYYY MM DD'); +SELECT to_date( '0000 06 30', 'YYYY MM DD'); -- should fail: there is no "0" year +SELECT to_date('-0001 06 30', 'YYYY MM DD'); + +SELECT to_date( '000 06 30', 'YYY MM DD'); -- OK, '2000-06-30' +SELECT to_date('-000 06 30', 'YYY MM DD'); -- should fail, fallbacks to "YYYY", 0 year +SELECT to_date('-001 06 30', 'YYY MM DD'); -- OK, fallbacks to "YYYY", 1BC year +SELECT to_date( '-01 06 30', 'YYY MM DD'); + +SELECT to_date( '-1 06 30 BC', 'CC MM DD BC'); -- "BC" is ignored when year is negative +SELECT to_date( '-1 06 30 AD', 'CC MM DD BC'); + +SELECT to_date( '-0001 06 30 BC', 'YYYY MM DD BC'); -- "BC" is ignored when year is negative +SELECT to_date( '-0001 06 30 AD', 'YYYY MM DD BC'); + +SELECT to_date('3220 184', 'IYYY IDDD'); +SELECT to_date('3220 184 BC', 'IYYY IDDD BC'); +SELECT to_date('-3220 184', 'IYYY IDDD'); +SELECT to_date('-3220 184 BC', 'IYYY IDDD BC'); -- "BC" is ignored when year is negative + +SELECT to_date('0000 184', 'IYYY IDDD'); -- check for errormsg ("IYYY" in detail) + +-- -- Check errors for some incorrect usages of to_timestamp() --