From 897ac0c6501cf85aadb7eeb6936894d27dcdade9 Mon Sep 17 00:00:00 2001 From: TreeHunter <60896014+TreeHunter9@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:01:58 +0300 Subject: [PATCH] Add FORMAT clause to convert datetime types to string and vice versa #2388 (#7629) * Add FORMAT clause to convert datetime types to string and vice versa * Add tests for FORMAT clause * Fixes after review * Change TZD to TZR * Change inline variables back to static * Add README documentation * Add ability to use " in raw string and ... Use session timezone if timezone is not specified. Add ability to use + sign in timezone offset. Add truncating string exception. * Move util methods from BOOST_AUTO_TEST_SUITE * Switch back to inline variables * Consider charset in the format string * Add ability to write patterns without separators * Use printf to add extra zeros Also add extra zeros to the year patterns. * Replace template exception with a plain function * Clean code after review * Fix bug with TZH:TZM when TZH is 0 * Add TZR to STRING to DATE --------- Co-authored-by: Artyom Ivanov --- doc/README.cast.format.md | 95 ++ src/common/ParserTokens.h | 1 + src/common/TimeZoneUtil.cpp | 28 +- src/common/TimeZoneUtil.h | 2 + src/common/classes/NoThrowTimeStamp.cpp | 74 ++ src/common/classes/NoThrowTimeStamp.h | 4 + src/common/common.h | 30 +- src/common/cvt.cpp | 1107 ++++++++++++++++++++++- src/common/cvt.h | 2 + src/common/tests/CvtTest.cpp | 321 +++++++ src/common/tests/CvtTestUtils.h | 126 +++ src/dsql/ExprNodes.cpp | 136 +-- src/dsql/ExprNodes.h | 6 +- src/dsql/parse.y | 128 +-- src/include/firebird/impl/blr.h | 2 + src/include/firebird/impl/msg/jrd.h | 9 + src/include/gen/Firebird.pas | 9 + 17 files changed, 1947 insertions(+), 133 deletions(-) create mode 100644 doc/README.cast.format.md create mode 100644 src/common/tests/CvtTest.cpp create mode 100644 src/common/tests/CvtTestUtils.h diff --git a/doc/README.cast.format.md b/doc/README.cast.format.md new file mode 100644 index 0000000000..9efcfee4d3 --- /dev/null +++ b/doc/README.cast.format.md @@ -0,0 +1,95 @@ +## 1. DATETIME TO STRING + +The following flags are currently implemented for datetime to string conversion: +| Format Pattern | Description | +| -------------- | ----------- | +| YEAR | Year (1 - 9999) | +| YYYY | Last 4 digits of Year (0001 - 9999) | +| YYY | Last 3 digits of Year (000 - 999) | +| YY | Last 2 digits of Year (00 - 99) | +| Y | Last 1 digits of Year (0 - 9) | +| Q | Quarter of the Year (1 - 4) | +| MM | Month (01 - 12) | +| MON | Short Month name (Apr) | +| MONTH | Full Month name (APRIL) | +| RM | Roman representation of the Month (I - XII) | +| WW | Week of the Year (01 - 53) | +| W | Week of the Month (1 - 5) | +| D | Day of the Week (1 - 7) | +| DAY | Full name of the Day (MONDAY) | +| DD | Day of the Month (01 - 31) | +| DDD | Day of the Year (001 - 366) | +| DY | Short name of the Day (Mon) | +| J | Julian Day (number of days since January 1, 4712 BC) | +| HH / HH12 | Hour of the Day (01 - 12) with period (AM, PM) | +| HH24 | Hour of the Day (00 - 23) | +| MI | Minutes (00 - 59) | +| SS | Seconds (00 - 59) | +| SSSSS | Seconds after midnight (0 - 86399) | +| FF1 - FF9 | Fractional seconds with the specified accuracy | +| TZH | Time zone in Hours (-14 - 14) | +| TZM | Time zone in Minutes (00 - 59) | +| TZR | Time zone Name | + +The dividers are: +| Dividers | +| ------------- | +| . | +| / | +| , | +| ; | +| : | +| 'space' | +| - | + +Patterns can be used without any dividers: +``` +SELECT CAST(CURRENT_TIMESTAMP AS VARCHAR(50) FORMAT 'YEARMMDD HH24MISS') FROM RDB$DATABASE; +========================= +20230719 161757 +``` +However, be careful with patterns like `DDDDD`, it will be interpreted as `DDD` + `DD`. + +It is possible to insert raw text into a format string with `""`: `... FORMAT '"Today is" DAY'` - Today is MONDAY. To add `"` in output raw string use `\"` (to print `\` use `\\`). +Also the format is case-insensitive, so `YYYY-MM` == `yyyy-mm`. +Example: +``` +SELECT CAST(CURRENT_TIMESTAMP AS VARCHAR(45) FORMAT 'DD.MM.YEAR HH24:MI:SS "is" J "Julian day"') FROM RDB$DATABASE; +========================= +14.6.2023 15:41:29 is 2460110 Julian day +``` + +## 2. STRING TO DATETIME + +The following flags are currently implemented for string to datetime conversion: +| Format Pattern | Description | +| ------------- | ------------- | +| YEAR | Year | +| YYYY | Last 4 digits of Year | +| YYY | Last 3 digits of Year | +| YY | Last 2 digits of Year | +| Y | Last 1 digits of Year | +| MM | Month (1 - 12) | +| MON | Short Month name (Apr) | +| MONTH | Full Month name (APRIL) | +| RM | Roman representation of the Month (I - XII) | +| DD | Day of the Month (1 - 31) | +| J | Julian Day (number of days since January 1, 4712 BC) | +| HH / HH12 | Hour of the Day (1 - 12) with period (AM, PM) | +| HH24 | Hour of the Day (0 - 23) | +| MI | Minutes (0 - 59) | +| SS | Seconds (0 - 59) | +| SSSSS | Seconds after midnight (0 - 86399) | +| FF1 - FF4 | Fractional seconds with the specified accuracy | +| TZH | Time zone in Hours (-14 - 14) | +| TZM | Time zone in Minutes (0 - 59) | +| TZR | Time zone Name | + +Dividers are the same as for datetime to string conversion and can also be omitted. + +Example: +``` +SELECT CAST('2000.12.08 12:35:30.5000' AS TIMESTAMP FORMAT 'YEAR.MM.DD HH24:MI:SS.FF4') FROM RDB$DATABASE; +===================== +2000-12-08 12:35:30.5000 +``` diff --git a/src/common/ParserTokens.h b/src/common/ParserTokens.h index 92f769f7fe..2bff7a923d 100644 --- a/src/common/ParserTokens.h +++ b/src/common/ParserTokens.h @@ -222,6 +222,7 @@ PARSER_TOKEN(TOK_FLOOR, "FLOOR", true) PARSER_TOKEN(TOK_FOLLOWING, "FOLLOWING", true) PARSER_TOKEN(TOK_FOR, "FOR", false) PARSER_TOKEN(TOK_FOREIGN, "FOREIGN", false) +PARSER_TOKEN(TOK_FORMAT, "FORMAT", true) PARSER_TOKEN(TOK_FREE_IT, "FREE_IT", true) PARSER_TOKEN(TOK_FROM, "FROM", false) PARSER_TOKEN(TOK_FULL, "FULL", false) diff --git a/src/common/TimeZoneUtil.cpp b/src/common/TimeZoneUtil.cpp index 712f3e8d5f..d0984db0ee 100644 --- a/src/common/TimeZoneUtil.cpp +++ b/src/common/TimeZoneUtil.cpp @@ -80,7 +80,6 @@ namespace static const TimeZoneDesc* getDesc(USHORT timeZone); static inline bool isOffset(USHORT timeZone); -static USHORT makeFromOffset(int sign, unsigned tzh, unsigned tzm); static inline SSHORT offsetZoneToDisplacement(USHORT timeZone); static inline USHORT displacementToOffsetZone(SSHORT displacement); static int parseNumber(const char*& p, const char* end); @@ -634,6 +633,20 @@ void TimeZoneUtil::extractOffset(const ISC_TIME_TZ& timeTz, SSHORT* offset) extractOffset(tsTz, offset); } +// Makes a time zone id from offsets. +USHORT TimeZoneUtil::makeFromOffset(int sign, unsigned tzh, unsigned tzm) +{ + if (!TimeZoneUtil::isValidOffset(sign, tzh, tzm)) + { + string str; + str.printf("%s%02u:%02u", (sign == -1 ? "-" : "+"), tzh, tzm); + status_exception::raise(Arg::Gds(isc_invalid_timezone_offset) << str); + } + + return (USHORT)displacementToOffsetZone((tzh * 60 + tzm) * sign); +} + + // Converts a time from local to UTC. void TimeZoneUtil::localTimeToUtc(ISC_TIME& time, ISC_USHORT timeZone) { @@ -1156,19 +1169,6 @@ static inline bool isOffset(USHORT timeZone) return timeZone <= ONE_DAY * 2; } -// Makes a time zone id from offsets. -static USHORT makeFromOffset(int sign, unsigned tzh, unsigned tzm) -{ - if (!TimeZoneUtil::isValidOffset(sign, tzh, tzm)) - { - string str; - str.printf("%s%02u:%02u", (sign == -1 ? "-" : "+"), tzh, tzm); - status_exception::raise(Arg::Gds(isc_invalid_timezone_offset) << str); - } - - return (USHORT)displacementToOffsetZone((tzh * 60 + tzm) * sign); -} - // Gets the displacement from a offset-based time zone id. static inline SSHORT offsetZoneToDisplacement(USHORT timeZone) { diff --git a/src/common/TimeZoneUtil.h b/src/common/TimeZoneUtil.h index 6768bc288d..ad4ab30569 100644 --- a/src/common/TimeZoneUtil.h +++ b/src/common/TimeZoneUtil.h @@ -97,6 +97,8 @@ public: static void extractOffset(const ISC_TIMESTAMP_TZ& timeStampTz, SSHORT* offset); static void extractOffset(const ISC_TIME_TZ& timeTz, SSHORT* offset); + static USHORT makeFromOffset(int sign, unsigned tzh, unsigned tzm); + static void localTimeToUtc(ISC_TIME& time, ISC_USHORT timeZone); static void localTimeToUtc(ISC_TIME_TZ& timeTz); diff --git a/src/common/classes/NoThrowTimeStamp.cpp b/src/common/classes/NoThrowTimeStamp.cpp index 8542e0ee69..0384cfada2 100644 --- a/src/common/classes/NoThrowTimeStamp.cpp +++ b/src/common/classes/NoThrowTimeStamp.cpp @@ -337,6 +337,80 @@ void NoThrowTimeStamp::round_time(ISC_TIME &ntime, const int precision) ntime -= (ntime % period); } +int NoThrowTimeStamp::convertGregorianDateToWeekDate(const struct tm& times) +{ + // Algorithm for Converting Gregorian Dates to ISO 8601 Week Date by Rick McCarty, 1999 + // http://personal.ecu.edu/mccartyr/ISOwdALG.txt + + const int y = times.tm_year + 1900; + const int dayOfYearNumber = times.tm_yday + 1; + + // Find the jan1Weekday for y (Monday=1, Sunday=7) + const int yy = (y - 1) % 100; + const int c = (y - 1) - yy; + const int g = yy + yy / 4; + const int jan1Weekday = 1 + (((((c / 100) % 4) * 5) + g) % 7); + + // Find the weekday for y m d + const int h = dayOfYearNumber + (jan1Weekday - 1); + const int weekday = 1 + ((h - 1) % 7); + + // Find if y m d falls in yearNumber y-1, weekNumber 52 or 53 + int yearNumber, weekNumber; + + if ((dayOfYearNumber <= (8 - jan1Weekday)) && (jan1Weekday > 4)) + { + yearNumber = y - 1; + weekNumber = ((jan1Weekday == 5) || ((jan1Weekday == 6) && + isLeapYear(yearNumber))) ? 53 : 52; + } + else + { + yearNumber = y; + + // Find if y m d falls in yearNumber y+1, weekNumber 1 + int i = isLeapYear(y) ? 366 : 365; + + if ((i - dayOfYearNumber) < (4 - weekday)) + { + yearNumber = y + 1; + weekNumber = 1; + } + } + + // Find if y m d falls in yearNumber y, weekNumber 1 through 53 + if (yearNumber == y) + { + int j = dayOfYearNumber + (7 - weekday) + (jan1Weekday - 1); + weekNumber = j / 7; + if (jan1Weekday > 4) + weekNumber--; + } + + return weekNumber; +} + +int NoThrowTimeStamp::convertGregorianDateToJulianDate(int year, int month, int day) +{ + int jdn = (1461 * (year + 4800 + (month - 14)/12))/4 + (367 * (month - 2 - 12 * ((month - 14)/12))) + / 12 - (3 * ((year + 4900 + (month - 14)/12)/100))/4 + day - 32075; + return jdn; +} + +void NoThrowTimeStamp::convertJulianDateToGregorianDate(int jdn, int& outYear, int& outMonth, int& outDay) +{ + int a = jdn + 32044; + int b = (4 * a +3 ) / 146097; + int c = a - (146097 * b) / 4; + int d = (4 * c + 3) / 1461; + int e = c - (1461 * d) / 4; + int m = (5 * e + 2) / 153; + + outDay = e - (153 * m + 2) / 5 + 1; + outMonth = m + 3 - 12 * (m / 10); + outYear = 100 * b + d - 4800 + (m / 10); +} + // Encode timestamp from UNIX datetime structure void NoThrowTimeStamp::encode(const struct tm* times, int fractions) { diff --git a/src/common/classes/NoThrowTimeStamp.h b/src/common/classes/NoThrowTimeStamp.h index 3ea51f6e30..58c126af5d 100644 --- a/src/common/classes/NoThrowTimeStamp.h +++ b/src/common/classes/NoThrowTimeStamp.h @@ -168,6 +168,10 @@ public: static void add10msec(ISC_TIMESTAMP* v, SINT64 msec, SINT64 multiplier); static void round_time(ISC_TIME& ntime, const int precision); + static int convertGregorianDateToWeekDate(const struct tm& times); + static int convertGregorianDateToJulianDate(int year, int month, int day); + static void convertJulianDateToGregorianDate(int jdn, int& outYear, int& outMonth, int& outDay); + static inline bool isLeapYear(const int year) noexcept { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); diff --git a/src/common/common.h b/src/common/common.h index 2eac429923..2162205a3b 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -971,7 +971,7 @@ const int HIGH_WORD = 0; #endif #endif -static const TEXT FB_SHORT_MONTHS[][4] = +inline const TEXT FB_SHORT_MONTHS[][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", @@ -980,7 +980,7 @@ static const TEXT FB_SHORT_MONTHS[][4] = "\0" }; -static const TEXT* const FB_LONG_MONTHS_UPPER[] = +inline const TEXT* const FB_LONG_MONTHS_UPPER[] = { "JANUARY", "FEBRUARY", @@ -997,6 +997,32 @@ static const TEXT* const FB_LONG_MONTHS_UPPER[] = 0 }; +// Starts with SUNDAY cuz tm.tm_wday starts with it +inline const TEXT FB_SHORT_DAYS[][4] = +{ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "\0" +}; + +// Starts with SUNDAY cuz tm.tm_wday starts with it +inline const TEXT* const FB_LONG_DAYS_UPPER[] = +{ + "SUNDAY", + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "\0" +}; + const FB_SIZE_T FB_MAX_SIZEOF = ~FB_SIZE_T(0); // Assume FB_SIZE_T is unsigned inline FB_SIZE_T fb_strlen(const char* str) diff --git a/src/common/cvt.cpp b/src/common/cvt.cpp index 7f7efabf03..e2cc65d8b7 100644 --- a/src/common/cvt.cpp +++ b/src/common/cvt.cpp @@ -58,6 +58,7 @@ #include "../common/utils_proto.h" #include "../common/StatusArg.h" #include "../common/status.h" +#include "../common/TimeZones.h" #ifdef HAVE_SYS_TYPES_H @@ -105,7 +106,8 @@ using namespace Firebird; #define FLOAT_MAX 3.402823466E+38F // max float (32 bit) value #endif -#define LETTER7(c) ((c) >= 'A' && (c) <= 'Z') +#define LETTER7_UPPER(c) ((c) >= 'A' && (c) <= 'Z') +#define LETTER(c) (((c) >= 'a' && (c) <= 'z') || ((c) >= 'A' && (c) <= 'Z')) #define DIGIT(c) ((c) >= '0' && (c) <= '9') #define ABSOLUT(x) ((x) < 0 ? -(x) : (x)) @@ -145,6 +147,126 @@ static void make_null_string(const dsc*, USHORT, const char**, vary*, USHORT, Fi class DummyException {}; +namespace +{ + class TimeZoneTrie + { + public: + static constexpr int EnglishAlphabet = 26; + static constexpr int Digits = 10; + static constexpr int OtherSymbols = 4; // '/' + '-' + '+' + '_' + + static constexpr int MaxPatternDiversity = EnglishAlphabet + Digits + OtherSymbols; + + static constexpr USHORT UninitializedTimezoneId = 0; + + struct TrieNode + { + AutoPtr childrens[MaxPatternDiversity]; + USHORT timezoneId = UninitializedTimezoneId; + }; + + TimeZoneTrie(MemoryPool& pool) + : m_root(FB_NEW_POOL(pool) TrieNode()), m_pool(pool) + { + USHORT id = 0; + for (const char* p : BUILTIN_TIME_ZONE_LIST) + { + string timezoneName(p); + timezoneName.upper(); + insertValue(timezoneName.c_str(), MAX_USHORT - id++); + } + } + + bool contains(const char* value, USHORT& outTimezoneId, int& outParsedTimezoneLength) + { + const TrieNode* currentNode = m_root; + FB_SIZE_T valueLength = fb_strlen(value); + + for (outParsedTimezoneLength = 0; outParsedTimezoneLength < valueLength; outParsedTimezoneLength++) + { + int index = calculateIndex(value[outParsedTimezoneLength]); + + if (index < 0 || currentNode->childrens[index] == nullptr) + break; + currentNode = currentNode->childrens[index]; + } + + if (currentNode->timezoneId == UninitializedTimezoneId) + return false; + + outTimezoneId = currentNode->timezoneId; + return true; + } + + private: + void insertValue(const char* value, USHORT timezoneId) + { + TrieNode* currentNode = m_root; + FB_SIZE_T valueLength = fb_strlen(value); + + for (int i = 0; i < valueLength; i++) + { + int index = calculateIndex(value[i]); + + if (currentNode->childrens[index] == nullptr) + currentNode->childrens[index] = FB_NEW_POOL(m_pool) TrieNode(); + currentNode = currentNode->childrens[index]; + } + + currentNode->timezoneId = timezoneId; + } + + int calculateIndex(char symbol) const + { + int index = -1; + symbol = UPPER(symbol); + + if (symbol >= '0' && symbol <= '9') + index = symbol - '0'; + else if (symbol >= 'A' && symbol <= 'Z') + index = symbol - 'A' + Digits; + else + { + switch (symbol) + { + case '/': index = Digits + EnglishAlphabet; break; + case '-': index = Digits + EnglishAlphabet + 1; break; + case '+': index = Digits + EnglishAlphabet + 2; break; + case '_': index = Digits + EnglishAlphabet + 3; break; + } + } + + fb_assert(index < MaxPatternDiversity); + return index; + } + + private: + AutoPtr m_root; + MemoryPool& m_pool; + }; +} + +enum class ExpectedDateType +{ + TIME, + DATE, + TIMEZONE +}; + +static const char* const TO_DATETIME_PATTERNS[] = { + "YEAR", "YYYY", "YYY", "YY", "Y", "Q", "MM", "MON", "MONTH", "RM", "WW", "W", + "D", "DAY", "DD", "DDD", "DY", "J", "HH", "HH12", "HH24", "MI", "SS", "SSSSS", + "FF1", "FF2", "FF3", "FF4", "FF5", "FF6", "FF7", "FF8", "FF9", "TZH", "TZM", "TZR" +}; + +static const char* const TO_STRING_PATTERNS[] = { + "YEAR", "YYYY", "YYY", "YY", "Y", "MM", "MON", "MONTH", "RM", "DD", "J", "HH", "HH12", + "HH24", "MI", "SS", "SSSSS", "FF1", "FF2", "FF3", "FF4", "TZH", "TZM", "TZR" +}; + +static InitInstance timeZoneTrie; + //#ifndef WORDS_BIGENDIAN //static const SQUAD quad_min_int = { 0, SLONG_MIN }; @@ -158,6 +280,12 @@ class DummyException {}; static const double eps_double = 1e-14; static const double eps_float = 1e-5; +template +static constexpr int sign(T value) +{ + return (value >= T(0)) ? 1 : -1; +} + static void float_to_text(const dsc* from, dsc* to, Callbacks* cb) { @@ -630,7 +758,7 @@ void CVT_string_to_datetime(const dsc* desc, } description[i] = precision; } - else if (LETTER7(c) && !have_english_month && i - start_component < 2) + else if (LETTER7_UPPER(c) && !have_english_month && i - start_component < 2) { TEXT temp[sizeof(YESTERDAY) + 1]; @@ -638,7 +766,7 @@ void CVT_string_to_datetime(const dsc* desc, while ((p < end) && (t < &temp[sizeof(temp) - 1])) { c = UPPER7(*p); - if (!LETTER7(c)) + if (!LETTER7_UPPER(c)) break; *t++ = c; p++; @@ -1033,6 +1161,979 @@ void CVT_string_to_datetime(const dsc* desc, } +static void date_type_check(ExpectedDateType expected, const dsc* desc, std::string_view pattern, Firebird::Callbacks* cb) +{ + switch (expected) + { + case ExpectedDateType::TIME: + if (!desc->isDate()) + return; + break; + + case ExpectedDateType::DATE: + if (!desc->isTime()) + return; + break; + + case ExpectedDateType::TIMEZONE: + if (desc->isDateTimeTz()) + return; + break; + + default: + break; + } + + cb->err(Arg::Gds(isc_incompatible_date_format_with_current_date_type) << string(pattern.data(), pattern.length())); +} + + +static string int_to_roman(int num) +{ + static const char* const symbols[] = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}; + static const int values[] = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; + + string roman; + for (int i = 0; i < 13; i++) + { + while (num >= values[i]) + { + roman += symbols[i]; + num -= values[i]; + } + } + return roman; +} + + +static SSHORT extract_timezone_offset(const dsc* desc) +{ + SSHORT timezoneOffset = 0; + + switch (desc->dsc_dtype) + { + case dtype_sql_time_tz: + case dtype_ex_time_tz: + TimeZoneUtil::extractOffset(*(ISC_TIME_TZ*) desc->dsc_address, &timezoneOffset); + break; + + case dtype_timestamp_tz: + case dtype_ex_timestamp_tz: + TimeZoneUtil::extractOffset(*(ISC_TIMESTAMP_TZ*) desc->dsc_address, &timezoneOffset); + break; + } + + return timezoneOffset; +} + + +static string extract_timezone_name(const dsc* desc) +{ + char timezoneBuffer[TimeZoneUtil::MAX_SIZE]; + unsigned int length = 0; + + switch (desc->dsc_dtype) + { + case dtype_sql_time_tz: + case dtype_ex_time_tz: + length = TimeZoneUtil::format(timezoneBuffer, sizeof(timezoneBuffer), + ((ISC_TIME_TZ*) desc->dsc_address)->time_zone); + break; + + case dtype_timestamp_tz: + case dtype_ex_timestamp_tz: + length = TimeZoneUtil::format(timezoneBuffer, sizeof(timezoneBuffer), + ((ISC_TIMESTAMP_TZ*) desc->dsc_address)->time_zone); + break; + } + + return string(timezoneBuffer, length); +} + + +static bool is_separator(char symbol) +{ + switch (symbol) + { + case '.': + case '/': + case ',': + case ';': + case ':': + case ' ': + case '-': + case '\"': + return true; + + default: + return false; + } +} + + +static void invalid_pattern_exception(std::string_view pattern, Callbacks* cb) +{ + cb->err(Arg::Gds(isc_invalid_date_format) << string(pattern.data(), pattern.length())); +} + + +static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::string_view pattern, + std::string_view previousPattern, const struct tm& times, int fractions, + Firebird::Callbacks* cb) +{ + string patternResult; + + switch (pattern[0]) + { + case 'Y': + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + int year = times.tm_year + 1900; + if (pattern == "Y") + patternResult.printf("%d", year % 10); + else if (pattern == "YY") + patternResult.printf("%02d", year % 100); + else if (pattern == "YYY") + patternResult.printf("%03d", year % 1000); + else if (pattern == "YYYY") + patternResult.printf("%04d", year % 10000); + else if (pattern == "YEAR") + patternResult.printf("%d", year); + break; + } + + case 'Q': + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + if (pattern == "Q") + { + int quarter = times.tm_mon / 3 + 1; + + patternResult.printf("%d", quarter); + } + break; + + case 'M': + if (pattern == "MI") + { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + + patternResult.printf("%02d", times.tm_min); + break; + } + + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + if (pattern == "MM") + patternResult.printf("%02d", (times.tm_mon + 1)); + else if (pattern == "MON") + patternResult.printf("%s", FB_SHORT_MONTHS[times.tm_mon]); + else if (pattern == "MONTH") + patternResult.printf("%s", FB_LONG_MONTHS_UPPER[times.tm_mon]); + break; + + case 'R': + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + if (pattern == "RM") + { + string roman = int_to_roman(times.tm_mon + 1); + patternResult.printf("%s", roman.c_str()); + } + break; + + case 'W': + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + if (pattern == "W") + { + int week = (times.tm_mday - 1) / 7; + patternResult.printf("%d", week + 1); + } + else if (pattern == "WW") + { + int week = NoThrowTimeStamp::convertGregorianDateToWeekDate(times); + patternResult.printf("%02d", week); + } + break; + + case 'D': + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + if (pattern == "D") + patternResult.printf("%d", times.tm_wday + 1); + else if (pattern == "DAY") + patternResult.printf("%s", FB_LONG_DAYS_UPPER[times.tm_wday]); + else if (pattern == "DD") + patternResult.printf("%02d", times.tm_mday); + else if (pattern == "DDD") + { + int daysInYear = times.tm_yday + 1; + patternResult.printf("%03d", daysInYear); + } + else if (pattern == "DY") + patternResult.printf("%s", FB_SHORT_DAYS[times.tm_wday]); + break; + + case 'J': + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + + if (pattern == "J") + { + int JulianDay = NoThrowTimeStamp::convertGregorianDateToJulianDate(times.tm_year + 1900, times.tm_mon + 1, + times.tm_mday); + + patternResult.printf("%d", JulianDay); + } + break; + + case 'H': + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + + if (pattern == "HH"|| + pattern == "HH12") + { + const char* period; + int hours = times.tm_hour; + + if (hours >= 12) + { + period = "PM"; + if (hours > 12) + hours -= 12; + } + else + { + period = "AM"; + if (hours == 0) + hours = 12; + } + + patternResult.printf("%02d %s", hours, period); + } + else if (pattern == "HH24") + patternResult.printf("%02d", times.tm_hour); + break; + + case 'S': + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + + if (pattern == "SS") + patternResult.printf("%02d", times.tm_sec); + else if (pattern == "SSSSS") + { + int secondsInDay = times.tm_hour * 60 * 60 + times.tm_min * 60 + times.tm_sec; + patternResult.printf("%d", secondsInDay); + } + break; + + case 'F': + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + + if (!strncmp(pattern.data(), "FF", pattern.length() - 1)) + { + int number = pattern.back() - '0'; + if (number < 1 || number > 9) + { + invalid_pattern_exception(pattern, cb); + } + + const int fractionsPrecision = fractions != 0 ? std::log10(fractions) + 1 : 1; + int additionalZerosCount = 0; + if (number > fractionsPrecision) + { + additionalZerosCount = number - fractionsPrecision; + number = fractionsPrecision; + } + + patternResult.printf("%d%.*s", fractions / (int) powf(10, fractionsPrecision - number), + additionalZerosCount, "00000000"); + } + break; + + case 'T': + date_type_check(ExpectedDateType::TIMEZONE, desc, pattern, cb); + + if (pattern == "TZH") + { + SSHORT timezoneOffset = extract_timezone_offset(desc); + int timezoneSign = sign(timezoneOffset); + SSHORT offsetInHours = abs(timezoneOffset / 60); + + string printfFormat = "%02d"; + + if (previousPattern != "TZM") + { + if (timezoneSign < 0) + printfFormat = "-" + printfFormat; + else + printfFormat = "+" + printfFormat; + } + + patternResult.printf(printfFormat.c_str(), offsetInHours); + } + else if (pattern == "TZM") + { + SSHORT timezoneOffset = extract_timezone_offset(desc); + int timezoneSign = sign(timezoneOffset); + SSHORT offsetInMinutes = abs(timezoneOffset % 60); + + string printfFormat = "%02d"; + + if (previousPattern != "TZH") + { + if (timezoneSign < 0) + printfFormat = "-" + printfFormat; + else if (timezoneOffset >= 0) + printfFormat = "+" + printfFormat; + } + + patternResult.printf(printfFormat.c_str(), offsetInMinutes); + } + else if (pattern == "TZR") + patternResult = extract_timezone_name(desc); + break; + + default: + invalid_pattern_exception(pattern, cb); + } + + if (patternResult.isEmpty()) + invalid_pattern_exception(pattern, cb); + + return patternResult; +} + + +string CVT_datetime_to_format_string(const dsc* desc, const string& format, Callbacks* cb) +{ + if (format.isEmpty()) + cb->err(Arg::Gds(isc_sysf_invalid_null_empty) << Arg::Str(STRINGIZE(format))); + + struct tm times; + memset(×, 0, sizeof(struct tm)); + + int fractions = 0; + + switch (desc->dsc_dtype) + { + case dtype_sql_time: + Firebird::TimeStamp::decode_time(*(GDS_TIME*) desc->dsc_address, + ×.tm_hour, ×.tm_min, ×.tm_sec, &fractions); + break; + + case dtype_sql_time_tz: + case dtype_ex_time_tz: + TimeZoneUtil::decodeTime(*(ISC_TIME_TZ*) desc->dsc_address, + true, TimeZoneUtil::NO_OFFSET, ×, &fractions); + break; + + case dtype_sql_date: + Firebird::TimeStamp::decode_date(*(GDS_DATE*) desc->dsc_address, ×); + break; + + case dtype_timestamp: + Firebird::TimeStamp::decode_timestamp(*(GDS_TIMESTAMP*) desc->dsc_address, ×, &fractions); + break; + + case dtype_timestamp_tz: + case dtype_ex_timestamp_tz: + TimeZoneUtil::decodeTimeStamp(*(ISC_TIMESTAMP_TZ*) desc->dsc_address, + true, TimeZoneUtil::NO_OFFSET, ×, &fractions); + break; + + default: + cb->err(Arg::Gds(isc_invalid_data_type_for_date_format)); + } + + string formatUpper(format); + for (int i = 0; i < formatUpper.length(); i++) + { + const char symbol = formatUpper[i]; + if (symbol != '\"') + { + formatUpper[i] = toupper(symbol); + continue; + } + + while(true) + { + int pos = formatUpper.find('\"', i + 1); + if (pos == string::npos) + cb->err(Arg::Gds(isc_invalid_raw_string_in_date_format)); + int tempPos = pos; + if (formatUpper[--tempPos] == '\\') + { + int backslashCount = 1; + while(formatUpper[--tempPos] == '\\') + backslashCount++; + if (backslashCount % 2 == 1) + { + i = pos; + continue; + } + } + i = pos; + break; + } + } + + string result; + int formatOffset = 0; + std::string_view pattern; + std::string_view previousPattern; + + for (int i = 0; i < formatUpper.length(); i++) + { + const char symbol = formatUpper[i]; + + if (is_separator(symbol)) + { + if (formatOffset != i) + { + result += datetime_to_format_string_pattern_matcher(desc, pattern, previousPattern, times, + fractions, cb); + previousPattern = pattern; + } + if (symbol == '\"') + { + i++; + int rawStringLength = formatUpper.length() - i; + string rawString(rawStringLength, '\0'); + for (int j = 0; j < rawStringLength; j++, i++) + { + if (formatUpper[i] == '\"') + break; + else if (formatUpper[i] == '\\') + rawString[j] = formatUpper[++i]; + else + rawString[j] = formatUpper[i]; + } + rawString.recalculate_length(); + result += rawString; + } + else + result += symbol; + + formatOffset = i + 1; + continue; + } + + pattern = std::string_view(formatUpper.c_str() + formatOffset, i - formatOffset + 1); + bool isFound = false; + for (int j = 0; j < FB_NELEM(TO_DATETIME_PATTERNS); j++) + { + if (!strncmp(TO_DATETIME_PATTERNS[j], pattern.data(), pattern.length())) + { + isFound = true; + if (i == formatUpper.length() - 1) + { + result += datetime_to_format_string_pattern_matcher(desc, pattern, previousPattern, times, + fractions, cb); + } + break; + } + } + if (isFound) + continue; + + if (pattern.length() <= 1) + invalid_pattern_exception(pattern, cb); + + pattern = pattern.substr(0, pattern.length() - 1); + result += datetime_to_format_string_pattern_matcher(desc, pattern, previousPattern, times, + fractions, cb); + previousPattern = pattern; + formatOffset = i; + i--; + } + + return result; +} + + +static int roman_to_int(const char* str, int length, int& offset) +{ + int result = 0; + int temp = 0; + + for (; offset < length; offset++) + { + int value = 0; + + switch (str[offset]) + { + case 'I': value = 1; break; + case 'V': value = 5; break; + case 'X': value = 10; break; + case 'L': value = 50; break; + case 'C': value = 100; break; + case 'D': value = 500; break; + case 'M': value = 1000; break; + default: return 0; + } + + result += value; + if (temp < value) + result -= temp * 2; + temp = value; + } + + return result; +} + + +static int parse_string_to_get_int(const char* str, int length, int& offset, int parseLength, bool withSign = false) +{ + int result = 0; + int sign = 1; + + if (withSign) + { + // To check '-' sign we need to move back, cuz '-' is also used as separator, + // so it will be skipped when we trying to remove "empty" space between values + if (str[offset] == '+') + offset++; + else if (offset != 0 && str[offset - 1] == '-') + sign = -1; + } + + const int parseLengthWithOffset = offset + parseLength; + for (; offset < parseLengthWithOffset && offset < length; offset++) + { + if (!DIGIT(str[offset])) + return result * sign; + + result = result * 10 + (str[offset] - '0'); + } + + return result * sign; +} + + +static std::string_view parse_string_to_get_first_word(const char* str, int length, int& offset, int parseLength) +{ + int wordLen = 0; + int startPoint = offset; + + const int parseLengthWithOffset = offset + parseLength; + for (; offset < parseLengthWithOffset && offset < length; offset++) + { + if (!LETTER(str[offset])) + break; + + ++wordLen; + } + + return std::string_view(str + startPoint, wordLen); +} + + +static void string_to_format_datetime_pattern_matcher(std::string_view pattern, std::string_view previousPattern, + const char* str, int strLength, int& strOffset, struct tm& outTimes, int& outFractions, + SSHORT& outTimezoneInMinutes, USHORT& outTimezoneId, Firebird::Callbacks* cb) +{ + switch (pattern[0]) + { + case 'Y': + if (pattern == "Y") + { + int year = parse_string_to_get_int(str, strLength, strOffset, 1); + outTimes.tm_year = 2000 + year - 1900; + return; + } + else if (pattern == "YY") + { + int year = parse_string_to_get_int(str, strLength, strOffset, 2); + outTimes.tm_year = (year > 45 ? 1900 + year : 2000 + year) - 1900; + return; + } + else if (pattern == "YYY") + { + int year = parse_string_to_get_int(str, strLength, strOffset, 3); + outTimes.tm_year = (year > 450 ? 1000 + year : 2000 + year) - 1900; + return; + } + else if (pattern == "YYYY") + { + int year = parse_string_to_get_int(str, strLength, strOffset, 4); + outTimes.tm_year = year - 1900; + return; + } + else if (pattern == "YEAR") + { + int year = parse_string_to_get_int(str, strLength, strOffset, strLength - strOffset); + if (year > 9999) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(0) << Arg::Num(9999)); + } + outTimes.tm_year = year - 1900; + return; + } + break; + + case 'M': + if (pattern == "MI") + { + int minutes = parse_string_to_get_int(str, strLength, strOffset, 2); + if (minutes > 59) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(0) << Arg::Num(59)); + } + + outTimes.tm_min = minutes; + return; + } + else if (pattern == "MM") + { + int month = parse_string_to_get_int(str, strLength, strOffset, 2); + if (month < 1 || month > 12) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(1) << Arg::Num(12)); + } + + outTimes.tm_mon = month - 1; + return; + } + else if (pattern == "MON") + { + std::string_view monthShortName = parse_string_to_get_first_word(str, strLength, strOffset, 3); + int month = -1; + for (int i = 0; i < FB_NELEM(FB_SHORT_MONTHS) - 1; i++) + { + if (std::equal(monthShortName.begin(), monthShortName.end(), + FB_SHORT_MONTHS[i], FB_SHORT_MONTHS[i] + strlen(FB_SHORT_MONTHS[i]), + [](char a, char b) { return a == UPPER(b); })) + { + outTimes.tm_mon = i; + return; + } + } + + cb->err(Arg::Gds(isc_month_name_mismatch) << string(monthShortName.data(), monthShortName.length())); + } + else if (pattern == "MONTH") + { + std::string_view monthFullName = parse_string_to_get_first_word(str, strLength, strOffset, strLength - strOffset); + for (int i = 0; i < FB_NELEM(FB_LONG_MONTHS_UPPER) - 1; i++) + { + if (std::equal(monthFullName.begin(), monthFullName.end(), + FB_LONG_MONTHS_UPPER[i], FB_LONG_MONTHS_UPPER[i] + strlen(FB_LONG_MONTHS_UPPER[i]), + [](char a, char b) { return a == UPPER(b); })) + { + outTimes.tm_mon = i; + return; + } + } + + cb->err(Arg::Gds(isc_month_name_mismatch) << string(monthFullName.data(), monthFullName.length())); + } + break; + + case 'R': + if (pattern == "RM") + { + int month = roman_to_int(str, strLength, strOffset); + if (month == 0 || month > 12) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(1) << Arg::Num(12)); + } + + outTimes.tm_mon = month - 1; + return; + } + break; + + case 'D': + if (pattern == "DD") + { + int day = parse_string_to_get_int(str, strLength, strOffset, 2); + if (day == 0 || day > 31) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(1) << Arg::Num(31)); + } + + outTimes.tm_mday = day; + return; + } + break; + + case 'J': + if (pattern == "J") + { + int JDN = parse_string_to_get_int(str, strLength, strOffset, strLength - strOffset); + + constexpr int minJDN = 1721426; // 0.0.0 + constexpr int maxJDN = 5373484; // 31.12.9999 + if (JDN < minJDN || JDN > maxJDN) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(minJDN) << Arg::Num(maxJDN)); + } + + int year, month, day; + NoThrowTimeStamp::convertJulianDateToGregorianDate(JDN, year, month, day); + outTimes.tm_year = year - 1900; + outTimes.tm_mon = month - 1; + outTimes.tm_mday = day; + return; + } + break; + + case 'H': + if (pattern == "HH"|| + pattern == "HH12") + { + int hours = parse_string_to_get_int(str, strLength, strOffset, 2); + if (hours > 12) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(0) << Arg::Num(12)); + } + + if (str[strOffset] == ' ') + strOffset++; + + std::string_view period = parse_string_to_get_first_word(str, strLength, strOffset, 2); + if (period == "AM") + { + outTimes.tm_hour = hours == 12 ? 0 : hours; + return; + } + else if (period == "PM") + { + outTimes.tm_hour = hours == 12 ? hours : 12 + hours; + return; + } + + cb->err(Arg::Gds(isc_incorrect_hours_period) << string(period.data(), period.length())); + } + else if (pattern == "HH24") + { + int hours = parse_string_to_get_int(str, strLength, strOffset, 2); + if (hours > 23) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(0) << Arg::Num(23)); + } + + outTimes.tm_hour = hours; + return; + } + break; + + case 'S': + if (pattern == "SS") + { + int seconds = parse_string_to_get_int(str, strLength, strOffset, 2); + if (seconds > 59) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(0) << Arg::Num(59)); + } + + outTimes.tm_sec = seconds; + return; + } + else if (pattern == "SSSSS") + { + constexpr int maximumSecondsInDay = 24 * 60 * 60 - 1; + + int secondsInDay = parse_string_to_get_int(str, strLength, strOffset, 5); + if (secondsInDay > maximumSecondsInDay) + { + cb->err(Arg::Gds(isc_value_for_pattern_is_out_of_range) << + string(pattern.data(), pattern.length()) << Arg::Num(0) << Arg::Num(maximumSecondsInDay)); + } + + int hours = secondsInDay / 24; + int minutes = secondsInDay / 60 - hours * 60; + int seconds = secondsInDay - minutes * 60 - hours * 60 * 60; + + outTimes.tm_hour = hours; + outTimes.tm_min = minutes; + outTimes.tm_sec = seconds; + return; + } + break; + + case 'F': + if (!strncmp(pattern.data(), "FF", pattern.length() - 1)) + { + int number = pattern.back() - '0'; + if (number < 1 || number > -ISC_TIME_SECONDS_PRECISION_SCALE) + { + invalid_pattern_exception(pattern, cb); + } + + const int fractions = parse_string_to_get_int(str, strLength, strOffset, number); + outFractions = fractions * pow(10, -ISC_TIME_SECONDS_PRECISION_SCALE - number); + return; + } + break; + + case 'T': + if (pattern == "TZH") + { + if (previousPattern == "TZM") + { + outTimezoneInMinutes += sign(outTimezoneInMinutes) * + parse_string_to_get_int(str, strLength, strOffset, strLength - strOffset) * 60; + } + else + outTimezoneInMinutes = parse_string_to_get_int(str, strLength, strOffset, strLength - strOffset, true) * 60; + return; + } + else if (pattern == "TZM") + { + if (previousPattern == "TZH") + { + outTimezoneInMinutes += sign(outTimezoneInMinutes) * + parse_string_to_get_int(str, strLength, strOffset, strLength - strOffset); + } + else + outTimezoneInMinutes = parse_string_to_get_int(str, strLength, strOffset, strLength - strOffset, true); + return; + } + else if (pattern == "TZR") + { + USHORT timezoneId; + + int parsedTimezoneNameLength; + bool timezoneNameIsCorrect = timeZoneTrie().contains(str + strOffset, outTimezoneId, parsedTimezoneNameLength); + if (!timezoneNameIsCorrect) + status_exception::raise(Arg::Gds(isc_invalid_timezone_region) << string(str + strOffset, parsedTimezoneNameLength)); + + strOffset += parsedTimezoneNameLength; + return; + } + break; + } + + invalid_pattern_exception(pattern, cb); +} + + +ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird::string& format, Firebird::Callbacks* cb) +{ + if (!DTYPE_IS_TEXT(desc->dsc_dtype)) + cb->err(Arg::Gds(isc_invalid_data_type_for_date_format)); + + if (format.isEmpty()) + cb->err(Arg::Gds(isc_sysf_invalid_null_empty) << Arg::Str(STRINGIZE(format))); + + USHORT dtype; + UCHAR* sourceString; + USHORT stringLength = CVT_get_string_ptr_common(desc, &dtype, &sourceString, nullptr, 0, 0, cb); + + string stringUpper(stringLength, '\0'); + for (int i = 0; i < stringLength; i++) + stringUpper[i] = toupper(sourceString[i]); + + string formatUpper(format.length(), '\0'); + for (int i = 0; i < format.length(); i++) + formatUpper[i] = toupper(format[i]); + + struct tm times; + memset(×, 0, sizeof(struct tm)); + times.tm_year = 1 - 1900; + times.tm_mday = 1; + + int fractions = 0; + + constexpr SSHORT uninitializedTimezoneOffsetValue = INT16_MIN; + SSHORT timezoneOffsetInMinutes = uninitializedTimezoneOffsetValue; + USHORT timezoneId = TimeZoneTrie::UninitializedTimezoneId; + + int formatOffset = 0; + int stringOffset = 0; + + std::string_view pattern; + std::string_view previousPattern; + + for (int i = 0; i < formatUpper.length(); i++) + { + const char symbol = formatUpper[i]; + + if (is_separator(symbol)) + { + if (formatOffset != i) + { + string_to_format_datetime_pattern_matcher(pattern, previousPattern, stringUpper.c_str(), + stringLength, stringOffset, times, fractions, timezoneOffsetInMinutes, timezoneId, cb); + previousPattern = pattern; + } + + formatOffset = i + 1; + continue; + } + + for (; stringOffset < stringLength; stringOffset++) + { + if (!is_separator(stringUpper[stringOffset])) + break; + } + + pattern = std::string_view(formatUpper.c_str() + formatOffset, i - formatOffset + 1); + bool isFound = false; + for (int j = 0; j < FB_NELEM(TO_STRING_PATTERNS); j++) + { + if (!strncmp(TO_STRING_PATTERNS[j], pattern.data(), pattern.length())) + { + isFound = true; + if (i == formatUpper.length() - 1) + { + string_to_format_datetime_pattern_matcher(pattern, previousPattern, stringUpper.c_str(), + stringLength, stringOffset, times, fractions, timezoneOffsetInMinutes, timezoneId, cb); + } + break; + } + } + if (isFound) + continue; + + if (pattern.length() <= 1) + invalid_pattern_exception(pattern, cb); + + pattern = pattern.substr(0, pattern.length() - 1); + string_to_format_datetime_pattern_matcher(pattern, previousPattern, stringUpper.c_str(), + stringLength, stringOffset, times, fractions, timezoneOffsetInMinutes, timezoneId, cb); + previousPattern = pattern; + formatOffset = i; + i--; + } + + for (; stringOffset < stringLength; stringOffset++) + { + if (!is_separator(stringUpper[stringOffset])) + break; + } + + if (stringOffset < stringLength) + cb->err(Arg::Gds(isc_trailing_part_of_string) << string(stringUpper.c_str() + stringOffset)); + + ISC_TIMESTAMP_TZ timestampTZ; + if (timezoneOffsetInMinutes == uninitializedTimezoneOffsetValue && timezoneId == TimeZoneTrie::UninitializedTimezoneId) + timestampTZ.time_zone = cb->getSessionTimeZone(); + else if (timezoneId != TimeZoneTrie::UninitializedTimezoneId) + timestampTZ.time_zone = timezoneId; + else + { + timestampTZ.time_zone = TimeZoneUtil::makeFromOffset(sign(timezoneOffsetInMinutes), + abs(timezoneOffsetInMinutes) / 60, abs(timezoneOffsetInMinutes) % 60); + } + timestampTZ.utc_timestamp = NoThrowTimeStamp::encode_timestamp(×, fractions); + + return timestampTZ; +} + + template void adjustForScale(V& val, SSHORT scale, const V limit, ErrorFunction err) { diff --git a/src/common/cvt.h b/src/common/cvt.h index a2e9dd3665..107a64c957 100644 --- a/src/common/cvt.h +++ b/src/common/cvt.h @@ -108,5 +108,7 @@ SQUAD CVT_get_quad(const dsc*, SSHORT, Firebird::DecimalStatus, ErrorFunction); void CVT_string_to_datetime(const dsc*, ISC_TIMESTAMP_TZ*, bool*, const Firebird::EXPECT_DATETIME, bool, Firebird::Callbacks*); const UCHAR* CVT_get_bytes(const dsc*, unsigned&); +Firebird::string CVT_datetime_to_format_string(const dsc* desc, const Firebird::string& format, Firebird::Callbacks* cb); +ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird::string& format, Firebird::Callbacks* cb); #endif //COMMON_CVT_H diff --git a/src/common/tests/CvtTest.cpp b/src/common/tests/CvtTest.cpp new file mode 100644 index 0000000000..6b8d689364 --- /dev/null +++ b/src/common/tests/CvtTest.cpp @@ -0,0 +1,321 @@ +#include "boost/test/unit_test.hpp" +#include "../common/tests/CvtTestUtils.h" +#include "../jrd/cvt_proto.h" + +using namespace Firebird; +using namespace Jrd; +using namespace CvtTestUtils; + +BOOST_AUTO_TEST_SUITE(CVTSuite) +BOOST_AUTO_TEST_SUITE(CVTDatetimeFormat) + +static void errFunc(const Firebird::Arg::StatusVector& v) +{ + v.raise(); +} + +CVTCallback cb(errFunc); + +BOOST_AUTO_TEST_SUITE(CVTDatetimeToFormatString) + +template +static void testCVTDatetimeToFormatString(T date, const string& format, const string& expected, Callbacks& cb) +{ + dsc desc; + desc.dsc_dtype = getDSCTypeFromDateType(); + desc.dsc_length = sizeof(T); + desc.dsc_address = (UCHAR*) &date; + desc.dsc_scale = 0; + + string result = CVT_datetime_to_format_string(&desc, format, &cb); + BOOST_TEST(result == expected, "\nRESULT: " << result.c_str() << "\nEXPECTED: " << expected.c_str()); +} + +BOOST_AUTO_TEST_SUITE(FunctionalTest) + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_DATE) +{ + testCVTDatetimeToFormatString(createDate(1, 1, 1), "YEAR.YYYY.YYY.YY.Y", "1.0001.001.01.1", cb); + testCVTDatetimeToFormatString(createDate(1234, 1, 1), "YEAR.YYYY.YYY.YY.Y", "1234.1234.234.34.4", cb); + testCVTDatetimeToFormatString(createDate(9999, 1, 1), "YEAR.YYYY.YYY.YY.Y", "9999.9999.999.99.9", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "YEAR.YYYY.YYY.YY.Y", "1.0001.001.01.1", cb); + testCVTDatetimeToFormatString(createDate(1234, 1, 1), "YEAR.YYYY.YYY.YY.Y", "1234.1234.234.34.4", cb); + testCVTDatetimeToFormatString(createDate(9999, 1, 1), "YEAR.YYYY.YYY.YY.Y", "9999.9999.999.99.9", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "Q", "1", cb); + testCVTDatetimeToFormatString(createDate(1, 4, 1), "Q", "2", cb); + testCVTDatetimeToFormatString(createDate(1, 7, 1), "Q", "3", cb); + testCVTDatetimeToFormatString(createDate(1, 10, 1), "Q", "4", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "MM:RM-MON/MONTH", "01:I-Jan/JANUARY", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 1), "MM-RM.MON;MONTH", "06-VI.Jun;JUNE", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 1), "MM,RM.MON:MONTH", "12,XII.Dec:DECEMBER", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "WW/W", "01/1", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 15), "WW-W", "24-3", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 30), "WW.W", "52.5", cb); + + testCVTDatetimeToFormatString(createDate(2023, 6, 4), "D;DAY-DY", "1;SUNDAY-Sun", cb); + testCVTDatetimeToFormatString(createDate(2023, 6, 7), "D.DAY,DY", "4.WEDNESDAY,Wed", cb); + testCVTDatetimeToFormatString(createDate(2023, 6, 10), "D DAY DY", "7 SATURDAY Sat", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "DDD", "001", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 12), "DDD", "012", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 15), "DDD", "166", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 31), "DDD", "365", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "J", "1721426", cb); + testCVTDatetimeToFormatString(createDate(2000, 12, 8), "J", "2451887", cb); + testCVTDatetimeToFormatString(createDate(9999, 12, 31), "J", "5373484", cb); +} + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_TIME) +{ + testCVTDatetimeToFormatString(createTime(0, 0, 0), "HH-HH12.HH24,MI/SS SSSSS", "12 AM-12 AM.00,00/00 0", cb); + testCVTDatetimeToFormatString(createTime(12, 35, 15), "HH.HH12:HH24;MI-SS/SSSSS", "12 PM.12 PM:12;35-15/45315", cb); + testCVTDatetimeToFormatString(createTime(23, 59, 59), " HH - HH12 . HH24 , MI / SS SSSSS ", " 11 PM - 11 PM . 23 , 59 / 59 86399 ", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0, 1), "FF1.FF2/FF3;FF4:FF5-FF6,FF7-FF8 FF9", "1.10/100;1000:10000-100000,1000000-10000000 100000000", cb); + testCVTDatetimeToFormatString(createTime(0, 0, 0, 1000), "FF1.FF2/FF3;FF4:FF5-FF6,FF7-FF8 FF9", "1.10/100;1000:10000-100000,1000000-10000000 100000000", cb); + testCVTDatetimeToFormatString(createTime(0, 0, 0, 9999), "FF1.FF2/FF3;FF4:FF5-FF6,FF7-FF8 FF9", "9.99/999;9999:99990-999900,9999000-99990000 999900000", cb); +} + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_TIMESTAMP) +{ + ISC_TIMESTAMP timestamp = createTimeStamp(1982, 4, 21, 1, 34, 15, 2500); + + testCVTDatetimeToFormatString(timestamp, "YEAR.YYYY.YYY.YY.Y/J", "1982.1982.982.82.2/2445081", cb); + testCVTDatetimeToFormatString(timestamp, "Q-MM-RM-MON-MONTH", "2-04-IV-Apr-APRIL", cb); + testCVTDatetimeToFormatString(timestamp, "WW,W-D;DAY:DD DDD.DY", "16,3-4;WEDNESDAY:21 111.Wed", cb); + testCVTDatetimeToFormatString(timestamp, "HH-HH12-HH24-MI-SS-SSSSS.FF2", "01 AM-01 AM-01-34-15-5655.25", cb); +} + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_TIME_TZ) +{ + testCVTDatetimeToFormatString(createTimeTZ(15, 35, 59, 0, 900), "HH-HH12-HH24-MI-SS-SSSSS.FF1/TZH/TZM", "03 PM-03 PM-15-35-59-56159.9/+00/00", cb); + testCVTDatetimeToFormatString(createTimeTZ(15, 35, 59, 160), "HH24:MI-TZH:TZM", "18:15-+02:40", cb); + testCVTDatetimeToFormatString(createTimeTZ(15, 35, 59, -160), "HH24:MI TZH:TZM", "12:55 -02:40", cb); + + testCVTDatetimeToFormatString(createTimeTZ(0, 0, 0, 160), "TZM:TZH", "+40:02", cb); + testCVTDatetimeToFormatString(createTimeTZ(0, 0, 0, 160), "TZH MI TZM", "+02 40 +40", cb); + testCVTDatetimeToFormatString(createTimeTZ(0, 0, 0, -160), "TZH MI TZM", "-02 20 -40", cb); +} + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_TIMESTAMP_TZ) +{ + ISC_TIMESTAMP_TZ timestampTZ = createTimeStampTZ(1982, 4, 21, 1, 34, 15, 0, 500); + + testCVTDatetimeToFormatString(timestampTZ, "YEAR.YYYY.YYY.YY.Y/J", "1982.1982.982.82.2/2445081", cb); + testCVTDatetimeToFormatString(timestampTZ, "Q-MM-RM-MON-MONTH", "2-04-IV-Apr-APRIL", cb); + testCVTDatetimeToFormatString(timestampTZ, "WW,W-D;DAY:DD DDD.DY", "16,3-4;WEDNESDAY:21 111.Wed", cb); + testCVTDatetimeToFormatString(timestampTZ, "HH-HH12-HH24-MI-SS-SSSSS.FF2/TZH/TZM", "01 AM-01 AM-01-34-15-5655.50/+00/00", cb); + + testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 1, 34, 15, 70), "HH24:MI-TZH:TZM", "02:44-+01:10", cb); + testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 1, 34, 15, -70), "HH24:MI TZH:TZM", "00:24 -01:10", cb); + + testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 0, 0, 0, 160), "TZM:TZH", "+40:02", cb); + testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 0, 0, 0, 160), "TZH MI TZM", "+02 40 +40", cb); + testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 0, 0, 0, -160), "TZH MI TZM", "-02 20 -40", cb); +} + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_SOLID_PATTERNS) +{ + ISC_TIMESTAMP_TZ timestampTZ = createTimeStampTZ(1982, 4, 21, 1, 34, 15, 0, 500); + + testCVTDatetimeToFormatString(timestampTZ, "YEARYYYYYYYYYYJ", "198219821982822445081", cb); + testCVTDatetimeToFormatString(timestampTZ, "QMMRMMONMONTH", "204IVAprAPRIL", cb); + testCVTDatetimeToFormatString(timestampTZ, "WWWD/DAYDDDDDDY", "1634/WEDNESDAY1111112", cb); + testCVTDatetimeToFormatString(timestampTZ, "HHHH12HH24MISSSSSSSFF2TZHTZM", "01 AM01 AM013456551550+0000", cb); +} + +BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_RAW_TEXT) +{ + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "YYYY-\"RaW TeXt\"-MON", "1981-RaW TeXt-Jul", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "YYYY-\"Raw Text with \\\"Quotes\\\"\"-MON", "1981-Raw Text with \"Quotes\"-Jul", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "YYYY-\"\\\\\\\"\\\\BS\\\\\\\"\\\\\"-YYYY", "1981-\\\"\\BS\\\"\\-1981", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "\"Test1\"-Y\"Test2\"", "Test1-1Test2", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "\"\"-Y\"Test2\"", "-1Test2", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "\"Test1\"-Y\"\"", "Test1-1", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "\"\"-Y\"\"", "-1", cb); + testCVTDatetimeToFormatString(createDate(1981, 7, 12), "\"\"\"\"", "", cb); +} + +BOOST_AUTO_TEST_SUITE_END() // FunctionalTest +BOOST_AUTO_TEST_SUITE_END() // CVTDatetimeToFormatString + +BOOST_AUTO_TEST_SUITE(CVTStringToFormatDateTime) + +static void testCVTStringToFormatDateTime(const string& date, const string& format, + const ISC_TIMESTAMP_TZ& expected, Callbacks& cb) +{ + string varyingString = "xx"; + varyingString += date; + *(USHORT*) varyingString.data() = varyingString.size() - sizeof(USHORT); + + dsc desc; + desc.dsc_dtype = dtype_varying; + desc.dsc_length = varyingString.size() + sizeof(USHORT); + desc.dsc_address = (UCHAR*) varyingString.data(); + desc.dsc_scale = 0; + + const ISC_TIMESTAMP_TZ result = CVT_string_to_format_datetime(&desc, format, &cb); + + struct tm resultTimes; + memset(&resultTimes, 0, sizeof(resultTimes)); + int resultFractions; + NoThrowTimeStamp::decode_timestamp(result.utc_timestamp, &resultTimes, &resultFractions); + SSHORT resultOffset; + TimeZoneUtil::extractOffset(result, &resultOffset); + + struct tm expectedTimes; + memset(&expectedTimes, 0, sizeof(expectedTimes)); + int expectedFractions; + NoThrowTimeStamp::decode_timestamp(expected.utc_timestamp, &expectedTimes, &expectedFractions); + SSHORT expectedOffset; + TimeZoneUtil::extractOffset(expected, &expectedOffset); + + bool isEqual = !((bool) memcmp(&resultTimes, &expectedTimes, sizeof(struct tm))) + && resultFractions == expectedFractions && resultOffset == expectedOffset; + + BOOST_TEST(isEqual, "\nRESULT: " << DECOMPOSE_TM_STRUCT(resultTimes, resultFractions, resultOffset) + << "\nEXPECTED: " << DECOMPOSE_TM_STRUCT(expectedTimes, expectedFractions, expectedOffset)); +} + +BOOST_AUTO_TEST_SUITE(FunctionalTest) + +BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_DATE) +{ + testCVTStringToFormatDateTime("1", "YEAR", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1234", "YEAR", createTimeStampTZ(1234, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("9999", "YEAR", createTimeStampTZ(9999, 1, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1", "YYYY", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1234", "YYYY", createTimeStampTZ(1234, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("9999", "YYYY", createTimeStampTZ(9999, 1, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1", "YYY", createTimeStampTZ(2001, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("450", "YYY", createTimeStampTZ(2450, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("451", "YYY", createTimeStampTZ(1451, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("999", "YYY", createTimeStampTZ(1999, 1, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1", "YY", createTimeStampTZ(2001, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("45", "YY", createTimeStampTZ(2045, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("46", "YY", createTimeStampTZ(1946, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("99", "YY", createTimeStampTZ(1999, 1, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1", "Y", createTimeStampTZ(2001, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("9", "Y", createTimeStampTZ(2009, 1, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1", "MM", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("6", "MM", createTimeStampTZ(1, 6, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("12", "MM", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("Jan", "MON", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("Jun", "MON", createTimeStampTZ(1, 6, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("Dec", "MON", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("January", "MONTH", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("June", "MONTH", createTimeStampTZ(1, 6, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("December", "MONTH", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("I", "RM", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("IV", "RM", createTimeStampTZ(1, 4, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("XII", "RM", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1", "DD", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("15", "DD", createTimeStampTZ(1, 1, 15, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("31", "DD", createTimeStampTZ(1, 1, 31, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("2451887", "J", createTimeStampTZ(2000, 12, 8, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1721426", "J", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("5373484", "J", createTimeStampTZ(9999, 12, 31, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("1:1,1", "YEAR.MM.DD", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1981-8/13", "YEAR.MM.DD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("9999 12;31", "YEAR.MM.DD", createTimeStampTZ(9999, 12, 31, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("25.Jan.25", "YY;MON;DD", createTimeStampTZ(2025, 1, 25, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("./.1981./-8--/13--", " YEAR. -.MM.,,-.DD//", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); +} + +BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_TIME) +{ + testCVTStringToFormatDateTime("12 AM", "HH", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1 AM", "HH", createTimeStampTZ(1, 1, 1, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTime("11 AM", "HH", createTimeStampTZ(1, 1, 1, 11, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("12 PM", "HH", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1 PM", "HH", createTimeStampTZ(1, 1, 1, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTime("11 PM", "HH", createTimeStampTZ(1, 1, 1, 23, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("12 AM", "HH12", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1 AM", "HH12", createTimeStampTZ(1, 1, 1, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTime("11 AM", "HH12", createTimeStampTZ(1, 1, 1, 11, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("12 PM", "HH12", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTime("1 PM", "HH12", createTimeStampTZ(1, 1, 1, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTime("11 PM", "HH12", createTimeStampTZ(1, 1, 1, 23, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("0", "HH24", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("12", "HH24", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTime("23", "HH24", createTimeStampTZ(1, 1, 1, 23, 0, 0, 0), cb); + + testCVTStringToFormatDateTime("0", "MI", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("30", "MI", createTimeStampTZ(1, 1, 1, 0, 30, 0, 0), cb); + testCVTStringToFormatDateTime("59", "MI", createTimeStampTZ(1, 1, 1, 0, 59, 0, 0), cb); + + testCVTStringToFormatDateTime("0", "SS", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("30", "SS", createTimeStampTZ(1, 1, 1, 0, 0, 30, 0), cb); + testCVTStringToFormatDateTime("59", "SS", createTimeStampTZ(1, 1, 1, 0, 0, 59, 0), cb); + + testCVTStringToFormatDateTime("0", "SSSSS", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTime("45315", "SSSSS", createTimeStampTZ(1, 1, 1, 12, 35, 15, 0), cb); + testCVTStringToFormatDateTime("86399", "SSSSS", createTimeStampTZ(1, 1, 1, 23, 59, 59, 0), cb); + + testCVTStringToFormatDateTime("1", "FF1", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTime("5", "FF1", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTime("9", "FF1", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9000), cb); + + testCVTStringToFormatDateTime("1", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 100), cb); + testCVTStringToFormatDateTime("10", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTime("50", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTime("99", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9900), cb); + + testCVTStringToFormatDateTime("1", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 10), cb); + testCVTStringToFormatDateTime("10", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 100), cb); + testCVTStringToFormatDateTime("100", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTime("500", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTime("999", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9990), cb); + + testCVTStringToFormatDateTime("1", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1), cb); + testCVTStringToFormatDateTime("10", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 10), cb); + testCVTStringToFormatDateTime("100", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 100), cb); + testCVTStringToFormatDateTime("1000", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTime("5000", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTime("9999", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9999), cb); + + testCVTStringToFormatDateTime("1 PM - 25 - 45 - 200", "HH.MI.SS.FF4", createTimeStampTZ(1, 1, 1, 13, 25, 45, 0, 200), cb); + testCVTStringToFormatDateTime("15:0:15:2", "HH24.MI.SS.FF1", createTimeStampTZ(1, 1, 1, 15, 0, 15, 0, 2000), cb); +} + +BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_TZ) +{ + testCVTStringToFormatDateTime("12:00 2:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 150, 0), cb); + testCVTStringToFormatDateTime("12:00 +2:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 150, 0), cb); + testCVTStringToFormatDateTime("12:00 -2:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, -150, 0), cb); + testCVTStringToFormatDateTime("12:00 +0:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 30, 0), cb); + testCVTStringToFormatDateTime("12:00 +0:00", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0, 0), cb); +} + +BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_SOLID_PATTERNS) +{ + testCVTStringToFormatDateTime("1 PM - 25 - 45 - 200", "HHMISSFF4", createTimeStampTZ(1, 1, 1, 13, 25, 45, 0, 200), cb); + testCVTStringToFormatDateTime("1981-8/13", "YEARMMDD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); +} + +BOOST_AUTO_TEST_SUITE_END() // FunctionalTest +BOOST_AUTO_TEST_SUITE_END() // CVTStringToFormatDateTime + +BOOST_AUTO_TEST_SUITE_END() // CVTDatetimeFormat +BOOST_AUTO_TEST_SUITE_END() // CVTSuite diff --git a/src/common/tests/CvtTestUtils.h b/src/common/tests/CvtTestUtils.h new file mode 100644 index 0000000000..dae4e53ecb --- /dev/null +++ b/src/common/tests/CvtTestUtils.h @@ -0,0 +1,126 @@ +#ifndef CVT_TEST_UTILS_H +#define CVT_TEST_UTILS_H + +#include "firebird.h" +#include "../common/dsc.h" +#include "../common/TimeZoneUtil.h" +#include "../common/TimeZones.h" + +using Firebird::NoThrowTimeStamp; +using Firebird::TimeZoneUtil; + +namespace CvtTestUtils { + +#define DECOMPOSE_TM_STRUCT(times, fractions, timezone) "Year:" << times.tm_year + 1900 << \ + " Month:" << times.tm_mon << \ + " Day:" << times.tm_mday << \ + " Hour:" << times.tm_hour << \ + " Min:" << times.tm_min << \ + " Sec:" << times.tm_sec << \ + " Fract: " << fractions << \ + " IsDST:" << times.tm_isdst << \ + " WDay:" << times.tm_wday << \ + " YDay:" << times.tm_yday << \ + " TZ Offset:" << timezone \ + +template +static constexpr int sign(T value) +{ + return (T(0) < value) - (value < T(0)); +} + +static struct tm initTMStruct(int year, int month, int day) +{ + struct tm times; + memset(×, 0, sizeof(struct tm)); + + times.tm_year = year - 1900; + times.tm_mon = month - 1; + times.tm_mday = day; + mktime(×); + + return times; +} + +static ISC_DATE createDate(int year, int month, int day) +{ + struct tm times = initTMStruct(year, month, day); + return NoThrowTimeStamp::encode_date(×); +} + +static ISC_TIME createTime(int hours, int minutes, int seconds, int fractions = 0) +{ + return NoThrowTimeStamp::encode_time(hours, minutes, seconds, fractions); +} + +static ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, int minutes, int seconds, int fractions = 0) +{ + struct tm times = initTMStruct(year, month, day); + times.tm_hour = hours; + times.tm_min = minutes; + times.tm_sec = seconds; + + return NoThrowTimeStamp::encode_timestamp(×, fractions); +} + +static ISC_TIME_TZ createTimeTZ(int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0) +{ + ISC_TIME_TZ timeTZ; + timeTZ.time_zone = TimeZoneUtil::makeFromOffset(sign(offsetInMinutes), abs(offsetInMinutes / 60), + abs(offsetInMinutes % 60)); + timeTZ.utc_time = createTime(hours, minutes, seconds, fractions); + + return timeTZ; +} + +static ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hours, int minutes, int seconds, + int offsetInMinutes, int fractions = 0) +{ + ISC_TIMESTAMP_TZ timestampTZ; + timestampTZ.time_zone = TimeZoneUtil::makeFromOffset(sign(offsetInMinutes), abs(offsetInMinutes / 60), + abs(offsetInMinutes % 60)); + timestampTZ.utc_timestamp = createTimeStamp(year, month, day, hours, minutes, seconds, fractions); + + return timestampTZ; +} + +class CVTCallback : public Firebird::Callbacks +{ +public: + explicit CVTCallback(ErrorFunction aErr) : Callbacks(aErr) + {} + +public: + bool transliterate(const dsc* from, dsc* to, CHARSET_ID&) override { return true; } + CHARSET_ID getChid(const dsc* d) override { return 0; } + Jrd::CharSet* getToCharset(CHARSET_ID charset2) override { return nullptr; } + void validateData(Jrd::CharSet* toCharset, SLONG length, const UCHAR* q) override { } + ULONG validateLength(Jrd::CharSet* charSet, CHARSET_ID charSetId, ULONG length, const UCHAR* start, + const USHORT size) override { return 0; } + SLONG getLocalDate() override { return 0; } + ISC_TIMESTAMP getCurrentGmtTimeStamp() override { ISC_TIMESTAMP ts; return ts; } + USHORT getSessionTimeZone() override { return 1439; } + void isVersion4(bool& v4) override { } +}; + +template +static UCHAR getDSCTypeFromDateType() { return 0; } + +template<> +UCHAR getDSCTypeFromDateType() { return dtype_sql_date; } + +template<> +UCHAR getDSCTypeFromDateType() { return dtype_sql_time; } + +template<> +UCHAR getDSCTypeFromDateType() { return dtype_timestamp; } + +template<> +UCHAR getDSCTypeFromDateType() { return dtype_sql_time_tz; } + +template<> +UCHAR getDSCTypeFromDateType() { return dtype_timestamp_tz; } + +} // namespace CvtTestUtils + +#endif // CVT_TEST_UTILS_H diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 57a7733413..21a438879d 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -3494,24 +3494,30 @@ dsc* BoolAsValueNode::execute(thread_db* tdbb, Request* request) const //-------------------- -static RegisterNode regCastNode({blr_cast}); +static RegisterNode regCastNode({blr_cast, blr_cast_format}); -CastNode::CastNode(MemoryPool& pool, ValueExprNode* aSource, dsql_fld* aDsqlField) +CastNode::CastNode(MemoryPool& pool, ValueExprNode* aSource, dsql_fld* aDsqlField, const string& aFormat) : TypedNode(pool), dsqlAlias("CAST"), dsqlField(aDsqlField), source(aSource), itemInfo(NULL), + format(pool, aFormat), artificial(false) { castDesc.clear(); } // Parse a datatype cast. -DmlNode* CastNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* csb, const UCHAR /*blrOp*/) +DmlNode* CastNode::parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* csb, const UCHAR blrOp) { + fb_assert(blrOp == blr_cast || blrOp == blr_cast_format); + CastNode* node = FB_NEW_POOL(pool) CastNode(pool); + if (blrOp == blr_cast_format) + csb->csb_blr_reader.getString(node->format); + ItemInfo itemInfo; PAR_desc(tdbb, csb, &node->castDesc, &itemInfo); @@ -3539,6 +3545,7 @@ string CastNode::internalPrint(NodePrinter& printer) const NODE_PRINT(printer, castDesc); NODE_PRINT(printer, source); NODE_PRINT(printer, itemInfo); + NODE_PRINT(printer, format); return "CastNode"; } @@ -3549,6 +3556,7 @@ ValueExprNode* CastNode::dsqlPass(DsqlCompilerScratch* dsqlScratch) node->dsqlAlias = dsqlAlias; node->source = doDsqlPass(dsqlScratch, source); node->dsqlField = dsqlField; + node->format = format; DDL_resolve_intl_type(dsqlScratch, node->dsqlField, NULL); node->setParameterType(dsqlScratch, NULL, false); @@ -3593,8 +3601,16 @@ bool CastNode::setParameterType(DsqlCompilerScratch* /*dsqlScratch*/, // Generate BLR for a data-type cast operation. void CastNode::genBlr(DsqlCompilerScratch* dsqlScratch) { - dsqlScratch->appendUChar(blr_cast); + if (format.hasData()) + { + dsqlScratch->appendUChar(blr_cast_format); + dsqlScratch->appendString(0, format); + } + else + dsqlScratch->appendUChar(blr_cast); + dsqlScratch->putDtype(dsqlField, true); + GEN_expr(dsqlScratch, source); } @@ -3640,6 +3656,7 @@ ValueExprNode* CastNode::copy(thread_db* tdbb, NodeCopier& copier) const node->source = copier.copy(tdbb, source); node->castDesc = castDesc; node->itemInfo = itemInfo; + node->format = format; return node; } @@ -3652,7 +3669,7 @@ bool CastNode::dsqlMatch(DsqlCompilerScratch* dsqlScratch, const ExprNode* other const CastNode* o = nodeAs(other); fb_assert(o); - return dsqlField == o->dsqlField; + return dsqlField == o->dsqlField && format == o->format; } bool CastNode::sameAs(const ExprNode* other, bool ignoreStreams) const @@ -3666,7 +3683,7 @@ bool CastNode::sameAs(const ExprNode* other, bool ignoreStreams) const const CastNode* const otherNode = nodeAs(other); fb_assert(otherNode); - return DSC_EQUIV(&castDesc, &otherNode->castDesc, true); + return DSC_EQUIV(&castDesc, &otherNode->castDesc, true) && format == otherNode->format; } ValueExprNode* CastNode::pass1(thread_db* tdbb, CompilerScratch* csb) @@ -3706,12 +3723,12 @@ dsc* CastNode::execute(thread_db* tdbb, Request* request) const const auto impure = request->getImpure(impureOffset); - return perform(tdbb, impure, value, &castDesc, itemInfo); + return perform(tdbb, impure, value, &castDesc, itemInfo, format); } // Cast from one datatype to another. dsc* CastNode::perform(thread_db* tdbb, impure_value* impure, dsc* value, - const dsc* castDesc, const ItemInfo* itemInfo) + const dsc* castDesc, const ItemInfo* itemInfo, const Firebird::string& format) { // If validation is not required and the source value is either NULL // or already in the desired data type, simply return it "as is" @@ -3767,7 +3784,58 @@ dsc* CastNode::perform(thread_db* tdbb, impure_value* impure, dsc* value, if (!value) return nullptr; - MOV_move(tdbb, value, &impure->vlu_desc); + + if (format.hasData()) + { + if (DTYPE_IS_TEXT(impure->vlu_desc.dsc_dtype)) + { + string result = CVT_datetime_to_format_string(value, format, &EngineCallbacks::instance); + USHORT dscLength = DSC_string_length(&impure->vlu_desc); + string::size_type resultLength = result.length(); + if (resultLength > dscLength) + { + ERR_post(Arg::Gds(isc_arith_except) << Arg::Gds(isc_string_truncation) << + Arg::Gds(isc_trunc_limits) << Arg::Num(dscLength) << Arg::Num(resultLength)); + } + + USHORT dscOffset = 0; + if (impure->vlu_desc.dsc_dtype == dtype_cstring) + dscOffset = 1; + else if (impure->vlu_desc.dsc_dtype == dtype_varying) + { + dscOffset = sizeof(USHORT); + ((vary*) impure->vlu_desc.dsc_address)->vary_length = resultLength; + } + + memcpy(impure->vlu_desc.dsc_address + dscOffset, result.c_str(), resultLength); + } + else + { + ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, &EngineCallbacks::instance); + switch (impure->vlu_desc.dsc_dtype) + { + case dtype_sql_time: + *(ISC_TIME*) impure->vlu_desc.dsc_address = timestampTZ.utc_timestamp.timestamp_time; + break; + case dtype_sql_date: + *(ISC_DATE*) impure->vlu_desc.dsc_address = timestampTZ.utc_timestamp.timestamp_date; + break; + case dtype_timestamp: + *(ISC_TIMESTAMP*) impure->vlu_desc.dsc_address = timestampTZ.utc_timestamp; + break; + case dtype_sql_time_tz: + case dtype_ex_time_tz: + *(ISC_TIME_TZ*) impure->vlu_desc.dsc_address = TimeZoneUtil::timeStampTzToTimeTz(timestampTZ); + break; + case dtype_timestamp_tz: + case dtype_ex_timestamp_tz: + *(ISC_TIMESTAMP_TZ*) impure->vlu_desc.dsc_address = timestampTZ; + break; + } + } + } + else + MOV_move(tdbb, value, &impure->vlu_desc); if (impure->vlu_desc.dsc_dtype == dtype_text) INTL_adjust_text_descriptor(tdbb, &impure->vlu_desc); @@ -5781,55 +5849,7 @@ dsc* ExtractNode::execute(thread_db* tdbb, Request* request) const case blr_extract_week: { - // Algorithm for Converting Gregorian Dates to ISO 8601 Week Date by Rick McCarty, 1999 - // http://personal.ecu.edu/mccartyr/ISOwdALG.txt - - const int y = times.tm_year + 1900; - const int dayOfYearNumber = times.tm_yday + 1; - - // Find the jan1Weekday for y (Monday=1, Sunday=7) - const int yy = (y - 1) % 100; - const int c = (y - 1) - yy; - const int g = yy + yy / 4; - const int jan1Weekday = 1 + (((((c / 100) % 4) * 5) + g) % 7); - - // Find the weekday for y m d - const int h = dayOfYearNumber + (jan1Weekday - 1); - const int weekday = 1 + ((h - 1) % 7); - - // Find if y m d falls in yearNumber y-1, weekNumber 52 or 53 - int yearNumber, weekNumber; - - if ((dayOfYearNumber <= (8 - jan1Weekday)) && (jan1Weekday > 4)) - { - yearNumber = y - 1; - weekNumber = ((jan1Weekday == 5) || ((jan1Weekday == 6) && - TimeStamp::isLeapYear(yearNumber))) ? 53 : 52; - } - else - { - yearNumber = y; - - // Find if y m d falls in yearNumber y+1, weekNumber 1 - int i = TimeStamp::isLeapYear(y) ? 366 : 365; - - if ((i - dayOfYearNumber) < (4 - weekday)) - { - yearNumber = y + 1; - weekNumber = 1; - } - } - - // Find if y m d falls in yearNumber y, weekNumber 1 through 53 - if (yearNumber == y) - { - int j = dayOfYearNumber + (7 - weekday) + (jan1Weekday - 1); - weekNumber = j / 7; - if (jan1Weekday > 4) - weekNumber--; - } - - part = weekNumber; + part = NoThrowTimeStamp::convertGregorianDateToWeekDate(times); break; } diff --git a/src/dsql/ExprNodes.h b/src/dsql/ExprNodes.h index a7ae73afc1..786a18e141 100644 --- a/src/dsql/ExprNodes.h +++ b/src/dsql/ExprNodes.h @@ -242,7 +242,8 @@ public: class CastNode final : public TypedNode { public: - explicit CastNode(MemoryPool& pool, ValueExprNode* aSource = NULL, dsql_fld* aDsqlField = NULL); + explicit CastNode(MemoryPool& pool, ValueExprNode* aSource = NULL, dsql_fld* aDsqlField = NULL, + const Firebird::string& aFormat = NULL); static DmlNode* parse(thread_db* tdbb, MemoryPool& pool, CompilerScratch* csb, const UCHAR blrOp); @@ -274,13 +275,14 @@ public: virtual dsc* execute(thread_db* tdbb, Request* request) const; static dsc* perform(thread_db* tdbb, impure_value* impure, dsc* value, - const dsc* castDesc, const ItemInfo* itemInfo); + const dsc* castDesc, const ItemInfo* itemInfo, const Firebird::string& format = nullptr); public: MetaName dsqlAlias; dsql_fld* dsqlField; NestConst source; NestConst itemInfo; + Firebird::string format; dsc castDesc; bool artificial; }; diff --git a/src/dsql/parse.y b/src/dsql/parse.y index ab093aa102..d8c6344d27 100644 --- a/src/dsql/parse.y +++ b/src/dsql/parse.y @@ -700,6 +700,7 @@ using namespace Firebird; %token ANY_VALUE %token CALL +%token FORMAT %token NAMED_ARG_ASSIGN // precedence declarations for expression evaluation @@ -4855,6 +4856,7 @@ non_charset_simple_type | numeric_type | float_type | decfloat_type + | date_time_type | BIGINT { $$ = newNode(); @@ -4898,60 +4900,6 @@ non_charset_simple_type $$->length = sizeof(SSHORT); $$->flags |= FLD_has_prec; } - | DATE - { - $$ = newNode(); - stmt_ambiguous = true; - - if (client_dialect <= SQL_DIALECT_V5) - { - // Post warning saying that DATE is equivalent to TIMESTAMP - ERRD_post_warning(Arg::Warning(isc_sqlwarn) << Arg::Num(301) << - Arg::Warning(isc_dtype_renamed)); - $$->dtype = dtype_timestamp; - $$->length = sizeof(GDS_TIMESTAMP); - } - else if (client_dialect == SQL_DIALECT_V6_TRANSITION) - yyabandon(YYPOSNARG(1), -104, isc_transitional_date); - else - { - $$->dtype = dtype_sql_date; - $$->length = sizeof(ULONG); - } - $$->flags |= FLD_has_prec; - } - | TIME without_time_zone_opt - { - $$ = newNode(); - - checkTimeDialect(); - $$->dtype = dtype_sql_time; - $$->length = sizeof(SLONG); - $$->flags |= FLD_has_prec; - } - | TIME WITH TIME ZONE - { - $$ = newNode(); - - checkTimeDialect(); - $$->dtype = dtype_sql_time_tz; - $$->length = sizeof(ISC_TIME_TZ); - $$->flags |= FLD_has_prec; - } - | TIMESTAMP without_time_zone_opt - { - $$ = newNode(); - $$->dtype = dtype_timestamp; - $$->length = sizeof(GDS_TIMESTAMP); - $$->flags |= FLD_has_prec; - } - | TIMESTAMP WITH TIME ZONE - { - $$ = newNode(); - $$->dtype = dtype_timestamp_tz; - $$->length = sizeof(ISC_TIMESTAMP_TZ); - $$->flags |= FLD_has_prec; - } | BOOLEAN { $$ = newNode(); @@ -8720,6 +8668,77 @@ named_argument cast_specification : CAST '(' value AS data_type_descriptor ')' { $$ = newNode($3, $5); } + | CAST '(' value AS cast_format_type cast_format_clause utf_string ')' + { $$ = newNode($3, $5, *$7); } + ; + +%type cast_format_clause +cast_format_clause + : FORMAT + ; + +%type date_time_type +date_time_type + : DATE + { + $$ = newNode(); + stmt_ambiguous = true; + + if (client_dialect <= SQL_DIALECT_V5) + { + // Post warning saying that DATE is equivalent to TIMESTAMP + ERRD_post_warning(Arg::Warning(isc_sqlwarn) << Arg::Num(301) << + Arg::Warning(isc_dtype_renamed)); + $$->dtype = dtype_timestamp; + $$->length = sizeof(GDS_TIMESTAMP); + } + else if (client_dialect == SQL_DIALECT_V6_TRANSITION) + yyabandon(YYPOSNARG(1), -104, isc_transitional_date); + else + { + $$->dtype = dtype_sql_date; + $$->length = sizeof(ULONG); + } + $$->flags |= FLD_has_prec; + } + | TIME without_time_zone_opt + { + $$ = newNode(); + + checkTimeDialect(); + $$->dtype = dtype_sql_time; + $$->length = sizeof(SLONG); + $$->flags |= FLD_has_prec; + } + | TIME WITH TIME ZONE + { + $$ = newNode(); + + checkTimeDialect(); + $$->dtype = dtype_sql_time_tz; + $$->length = sizeof(ISC_TIME_TZ); + $$->flags |= FLD_has_prec; + } + | TIMESTAMP without_time_zone_opt + { + $$ = newNode(); + $$->dtype = dtype_timestamp; + $$->length = sizeof(GDS_TIMESTAMP); + $$->flags |= FLD_has_prec; + } + | TIMESTAMP WITH TIME ZONE + { + $$ = newNode(); + $$->dtype = dtype_timestamp_tz; + $$->length = sizeof(ISC_TIMESTAMP_TZ); + $$->flags |= FLD_has_prec; + } + ; + +%type cast_format_type +cast_format_type + : character_type + | date_time_type ; // case expressions @@ -9383,6 +9402,7 @@ non_reserved_word | UNICODE_VAL // added in FB 6.0 | ANY_VALUE + | FORMAT ; %% diff --git a/src/include/firebird/impl/blr.h b/src/include/firebird/impl/blr.h index 644c4ea165..348037de2c 100644 --- a/src/include/firebird/impl/blr.h +++ b/src/include/firebird/impl/blr.h @@ -496,4 +496,6 @@ #define blr_default_arg (unsigned char) 227 +#define blr_cast_format (unsigned char) 228 + #endif // FIREBIRD_IMPL_BLR_H diff --git a/src/include/firebird/impl/msg/jrd.h b/src/include/firebird/impl/msg/jrd.h index 76901784dc..0c12476bf8 100644 --- a/src/include/firebird/impl/msg/jrd.h +++ b/src/include/firebird/impl/msg/jrd.h @@ -972,3 +972,12 @@ FB_IMPL_MSG(JRD, 969, uninitialized_var, -625, "42", "000", "Variable @1 is not FB_IMPL_MSG(JRD, 970, param_not_exist, -170, "07", "001", "Parameter @1 does not exist") FB_IMPL_MSG(JRD, 971, param_no_default_not_specified, -170, "07", "001", "Parameter @1 has no default value and was not specified or was specified with DEFAULT") FB_IMPL_MSG(JRD, 972, param_multiple_assignments, -170, "07", "001", "Parameter @1 has multiple assignments") +FB_IMPL_MSG(JRD, 973, invalid_date_format, -901, "HY", "000", "Cannot recognize \"@1\" part of date format") +FB_IMPL_MSG(JRD, 974, invalid_raw_string_in_date_format, -901, "HY", "000", "Cannot find closing \" for raw text in date format") +FB_IMPL_MSG(JRD, 975, invalid_data_type_for_date_format, -901, "HY", "000", "It is not possible to use this data type for date formatting") +FB_IMPL_MSG(JRD, 976, incompatible_date_format_with_current_date_type, -901, "HY", "000", "Cannot use \"@1\" format with current date type") +FB_IMPL_MSG(JRD, 977, value_for_pattern_is_out_of_range, -901, "HY", "000", "Value for @1 pattern is out of range [@2, @3]") +FB_IMPL_MSG(JRD, 978, month_name_mismatch, -901, "HY", "000", "@1 is not MONTH") +FB_IMPL_MSG(JRD, 979, incorrect_hours_period, -901, "HY", "000", "@1 is incorrect period for 12H, it should be AM or PM") +FB_IMPL_MSG(JRD, 980, data_for_format_is_exhausted, -901, "HY", "000", "All data has been read, but format pattern wants more. Unfilled patterns: \"@1\"") +FB_IMPL_MSG(JRD, 981, trailing_part_of_string, -901, "HY", "000", "There is a trailing part of input string that does not fit into FORMAT: \"@1\"") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index 0496fc4693..fec52a3055 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -5704,6 +5704,15 @@ const isc_param_not_exist = 335545290; isc_param_no_default_not_specified = 335545291; isc_param_multiple_assignments = 335545292; + isc_invalid_date_format = 335545293; + isc_invalid_raw_string_in_date_format = 335545294; + isc_invalid_data_type_for_date_format = 335545295; + isc_incompatible_date_format_with_current_date_type = 335545296; + isc_value_for_pattern_is_out_of_range = 335545297; + isc_month_name_mismatch = 335545298; + isc_incorrect_hours_period = 335545299; + isc_data_for_format_is_exhausted = 335545300; + isc_trailing_part_of_string = 335545301; isc_gfix_db_name = 335740929; isc_gfix_invalid_sw = 335740930; isc_gfix_incmp_sw = 335740932;