diff --git a/builds/posix/Makefile.in b/builds/posix/Makefile.in index 61a677c24f..6dee68ce86 100644 --- a/builds/posix/Makefile.in +++ b/builds/posix/Makefile.in @@ -790,18 +790,20 @@ install install-embedded silent_install package packages dist: .PHONY: tests tests_process run_tests run_tests_process +log_level ?= all + tests: $(MAKE) TARGET?=$(DefaultTarget) tests_process tests_process: $(COMMON_TEST) $(ENGINE_TEST) $(ISQL_TEST) run_tests: - $(MAKE) TARGET?=$(DefaultTarget) run_tests_process + $(MAKE) TARGET?=$(DefaultTarget) LOG_LEVEL?=$(log_level) run_tests_process run_tests_process: tests_process - $(COMMON_TEST) --log_level=all - $(ENGINE_TEST) --log_level=all - $(ISQL_TEST) --log_level=all + $(COMMON_TEST) --log_level=$(LOG_LEVEL) + $(ENGINE_TEST) --log_level=$(LOG_LEVEL) + $(ISQL_TEST) --log_level=$(LOG_LEVEL) #___________________________________________________________________________ diff --git a/doc/README.cast.format.md b/doc/README.cast.format.md index 9efcfee4d3..7305d9da31 100644 --- a/doc/README.cast.format.md +++ b/doc/README.cast.format.md @@ -1,6 +1,6 @@ ## 1. DATETIME TO STRING -The following flags are currently implemented for datetime to string conversion: +The following patterns are currently implemented for datetime to string conversion: | Format Pattern | Description | | -------------- | ----------- | | YEAR | Year (1 - 9999) | @@ -21,12 +21,13 @@ The following flags are currently implemented for datetime to string conversion: | 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) | +| HH / HH12 | Hour of the Day (01 - 12) without Period (for Period use A.M or P.M.) | | 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 | +| A.M. / P.M. | Period for 12 hours time (it doesn't matter which one is used, period will be inserted based on time) | | TZH | Time zone in Hours (-14 - 14) | | TZM | Time zone in Minutes (00 - 59) | | TZR | Time zone Name | @@ -61,7 +62,7 @@ SELECT CAST(CURRENT_TIMESTAMP AS VARCHAR(45) FORMAT 'DD.MM.YEAR HH24:MI:SS "is" ## 2. STRING TO DATETIME -The following flags are currently implemented for string to datetime conversion: +The following patterns are currently implemented for string to datetime conversion: | Format Pattern | Description | | ------------- | ------------- | | YEAR | Year | @@ -69,24 +70,39 @@ The following flags are currently implemented for string to datetime conversion: | YYY | Last 3 digits of Year | | YY | Last 2 digits of Year | | Y | Last 1 digits of Year | +| RR / RRRR | Round Year (further information below) | | 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) | +| HH / HH12 | Hour of the Day (1 - 12) without Period (to specify Period use A.M or P.M.) | | 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 | +| A.M. / P.M. | Period for 12 hours time (it doesn't matter which one is used, period will be taken from input string) | | 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. +Year, month and day will be taken from current date if these components are not used in pattern (this applies only to data types that contain a date component). + +Behavior of `RR`: +- If the specified two-digit year is 00 to 49, then + - If the last two digits of the current year are 00 to 49, then the returned year has the same first two digits as the current year. + - If the last two digits of the current year are 50 to 99, then the first 2 digits of the returned year are 1 greater than the first 2 digits of the current year. +- If the specified two-digit year is 50 to 99, then + - If the last two digits of the current year are 00 to 49, then the first 2 digits of the returned year are 1 less than the first 2 digits of the current year. + - If the last two digits of the current year are 50 to 99, then the returned year has the same first two digits as the current year. + +Behavior of `RRRR`: Accepts either 4-digit or 2-digit input. If 2-digit, provides the same return as `RR`. If you do not want this functionality, then enter the 4-digit year. + + Example: ``` SELECT CAST('2000.12.08 12:35:30.5000' AS TIMESTAMP FORMAT 'YEAR.MM.DD HH24:MI:SS.FF4') FROM RDB$DATABASE; diff --git a/src/common/common.h b/src/common/common.h index 2162205a3b..bddee3fe5d 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -971,6 +971,7 @@ const int HIGH_WORD = 0; #endif #endif + inline const TEXT FB_SHORT_MONTHS[][4] = { "Jan", "Feb", "Mar", diff --git a/src/common/cvt.cpp b/src/common/cvt.cpp index 2afa5cc1e7..8be4b19b68 100644 --- a/src/common/cvt.cpp +++ b/src/common/cvt.cpp @@ -250,28 +250,49 @@ namespace AutoPtr m_root; MemoryPool& m_pool; }; + + namespace Format + { + typedef unsigned Patterns; + + constexpr Patterns NONE = 0; + constexpr Patterns HH24 = 1 << 0; + constexpr Patterns HH12 = 1 << 1; + constexpr Patterns AM = 1 << 2; + constexpr Patterns PM = 1 << 3; + constexpr Patterns AM_OR_PM_FIRST = 1 << 4; + constexpr Patterns SSSSS = 1 << 5; + constexpr Patterns MI = 1 << 6; + constexpr Patterns SS = 1 << 7; + constexpr Patterns TZH = 1 << 8; + constexpr Patterns TZM = 1 << 9; + } + + enum class ExpectedDateType + { + TIME, + DATE, + TIMEZONE + }; + + constexpr char AM_PERIOD[] = "A.M."; + constexpr char PM_PERIOD[] = "P.M."; + + 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", + AM_PERIOD, PM_PERIOD + }; + + const char* const TO_STRING_PATTERNS[] = { + "YEAR", "YYYY", "YYY", "YY", "Y", "RRRR", "RR", "MM", "MON", "MONTH", "RM", "DD", "J", "HH", "HH12", + "HH24", "MI", "SS", "SSSSS", "FF1", "FF2", "FF3", "FF4", "TZH", "TZM", "TZR", AM_PERIOD, PM_PERIOD + }; + + InitInstance timeZoneTrie; } -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 }; @@ -373,6 +394,51 @@ static constexpr int sign(T value) return (value >= T(0)) ? 1 : -1; } +static void validateTimeStamp(const ISC_TIMESTAMP timestamp, const EXPECT_DATETIME expectedType, const dsc* desc, + Callbacks* cb) +{ + if (!NoThrowTimeStamp::isValidTimeStamp(timestamp)) + { + switch (expectedType) + { + case expect_sql_date: + cb->err(Arg::Gds(isc_date_range_exceeded)); + break; + case expect_sql_time: + case expect_sql_time_tz: + cb->err(Arg::Gds(isc_time_range_exceeded)); + break; + case expect_timestamp: + case expect_timestamp_tz: + cb->err(Arg::Gds(isc_datetime_range_exceeded)); + break; + default: // this should never happen! + CVT_conversion_error(desc, cb->err); + break; + } + } +} + +static void timeStampToUtc(ISC_TIMESTAMP_TZ& timestampTZ, USHORT sessionTimeZone, const EXPECT_DATETIME expectedType, + Callbacks* cb) +{ + if (expectedType == expect_sql_time_tz || expectedType == expect_timestamp_tz || timestampTZ.time_zone != sessionTimeZone) + TimeZoneUtil::localTimeStampToUtc(timestampTZ); + + if (timestampTZ.time_zone != sessionTimeZone) + { + if (expectedType == expect_sql_time) + { + ISC_TIME_TZ timeTz; + timeTz.utc_time = timestampTZ.utc_timestamp.timestamp_time; + timeTz.time_zone = timestampTZ.time_zone; + timestampTZ.utc_timestamp.timestamp_time = TimeZoneUtil::timeTzToTime(timeTz, cb); + } + else if (expectedType == expect_timestamp) + *(ISC_TIMESTAMP*) ×tampTZ = TimeZoneUtil::timeStampTzToTimeStamp(timestampTZ, sessionTimeZone); + } +} + static void float_to_text(const dsc* from, dsc* to, Callbacks* cb) { @@ -1183,27 +1249,7 @@ void CVT_string_to_datetime(const dsc* desc, // This catches things like 29-Feb-1995 (not a leap year) Firebird::TimeStamp ts(times); - - if (!ts.isValid()) - { - switch (expect_type) - { - case expect_sql_date: - cb->err(Arg::Gds(isc_date_range_exceeded)); - break; - case expect_sql_time: - case expect_sql_time_tz: - cb->err(Arg::Gds(isc_time_range_exceeded)); - break; - case expect_timestamp: - case expect_timestamp_tz: - cb->err(Arg::Gds(isc_datetime_range_exceeded)); - break; - default: // this should never happen! - CVT_conversion_error(desc, cb->err); - break; - } - } + validateTimeStamp(ts.value(), expect_type, desc, cb); if (expect_type != expect_sql_time && expect_type != expect_sql_time_tz) { @@ -1230,21 +1276,7 @@ void CVT_string_to_datetime(const dsc* desc, date->utc_timestamp.timestamp_time += components[6]; date->time_zone = zone; - if (expect_type == expect_sql_time_tz || expect_type == expect_timestamp_tz || zone != sessionTimeZone) - TimeZoneUtil::localTimeStampToUtc(*date); - - if (zone != sessionTimeZone) - { - if (expect_type == expect_sql_time) - { - ISC_TIME_TZ timeTz; - timeTz.utc_time = date->utc_timestamp.timestamp_time; - timeTz.time_zone = zone; - date->utc_timestamp.timestamp_time = TimeZoneUtil::timeTzToTime(timeTz, cb); - } - else if (expect_type == expect_timestamp) - *(ISC_TIMESTAMP*) date = TimeZoneUtil::timeStampTzToTimeStamp(*date, sessionTimeZone); - } + timeStampToUtc(*date, sessionTimeZone, expect_type, cb); } @@ -1293,6 +1325,26 @@ static string int_to_roman(int num) } +static std::pair calculate_12hours_from_24hours(int hours) +{ + const char* period = nullptr; + if (hours >= 12) + { + period = PM_PERIOD; + if (hours > 12) + hours -= 12; + } + else + { + period = AM_PERIOD; + if (hours == 0) + hours = 12; + } + + return { hours, period }; +} + + static SSHORT extract_timezone_offset(const dsc* desc) { SSHORT timezoneOffset = 0; @@ -1374,27 +1426,45 @@ static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::st { case 'Y': { - date_type_check(ExpectedDateType::DATE, desc, pattern, cb); - int year = times.tm_year + 1900; if (pattern == "Y") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%d", year % 10); + } else if (pattern == "YY") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%02d", year % 100); + } else if (pattern == "YYY") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%03d", year % 1000); + } else if (pattern == "YYYY") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%04d", year % 10000); + } else if (pattern == "YEAR") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%d", year); + } break; } case 'Q': - date_type_check(ExpectedDateType::DATE, desc, pattern, cb); - if (pattern == "Q") { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + int quarter = times.tm_mon / 3 + 1; patternResult.printf("%d", quarter); @@ -1410,64 +1480,92 @@ static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::st break; } - date_type_check(ExpectedDateType::DATE, desc, pattern, cb); - if (pattern == "MM") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%02d", (times.tm_mon + 1)); + } else if (pattern == "MON") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%s", FB_SHORT_MONTHS[times.tm_mon]); + } else if (pattern == "MONTH") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%s", FB_LONG_MONTHS_UPPER[times.tm_mon]); + } break; case 'R': - date_type_check(ExpectedDateType::DATE, desc, pattern, cb); - if (pattern == "RM") { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + 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") { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + int week = (times.tm_mday - 1) / 7; patternResult.printf("%d", week + 1); } else if (pattern == "WW") { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + int week = NoThrowTimeStamp::convertGregorianDateToWeekDate(times); patternResult.printf("%02d", week); } break; case 'D': - date_type_check(ExpectedDateType::DATE, desc, pattern, cb); - if (pattern == "D") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%d", times.tm_wday + 1); + } else if (pattern == "DAY") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%s", FB_LONG_DAYS_UPPER[times.tm_wday]); + } else if (pattern == "DD") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%02d", times.tm_mday); + } else if (pattern == "DDD") { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + int daysInYear = times.tm_yday + 1; patternResult.printf("%03d", daysInYear); } else if (pattern == "DY") + { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + patternResult.printf("%s", FB_SHORT_DAYS[times.tm_wday]); + } break; case 'J': - date_type_check(ExpectedDateType::DATE, desc, pattern, cb); - if (pattern == "J") { + date_type_check(ExpectedDateType::DATE, desc, pattern, cb); + int JulianDay = NoThrowTimeStamp::convertGregorianDateToJulianDate(times.tm_year + 1900, times.tm_mon + 1, times.tm_mday); @@ -1476,50 +1574,44 @@ static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::st break; case 'H': - date_type_check(ExpectedDateType::TIME, desc, pattern, cb); - if (pattern == "HH"|| pattern == "HH12") { - const char* period; - int hours = times.tm_hour; + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); - if (hours >= 12) - { - period = "PM"; - if (hours > 12) - hours -= 12; - } - else - { - period = "AM"; - if (hours == 0) - hours = 12; - } + const auto [hours, period] = calculate_12hours_from_24hours(times.tm_hour); - patternResult.printf("%02d %s", hours, period); + patternResult.printf("%02d", hours); } else if (pattern == "HH24") + { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + patternResult.printf("%02d", times.tm_hour); + } break; case 'S': - date_type_check(ExpectedDateType::TIME, desc, pattern, cb); - if (pattern == "SS") + { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + patternResult.printf("%02d", times.tm_sec); + } else if (pattern == "SSSSS") { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + 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)) { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + int number = pattern.back() - '0'; if (number < 1 || number > 9) { @@ -1539,11 +1631,33 @@ static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::st } break; - case 'T': - date_type_check(ExpectedDateType::TIMEZONE, desc, pattern, cb); + case 'A': + if (pattern == AM_PERIOD) + { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + const auto [hours, period] = calculate_12hours_from_24hours(times.tm_hour); + + patternResult.printf("%s", period); + } + break; + + case 'P': + if (pattern == PM_PERIOD) + { + date_type_check(ExpectedDateType::TIME, desc, pattern, cb); + + const auto [hours, period] = calculate_12hours_from_24hours(times.tm_hour); + + patternResult.printf("%s", period); + } + break; + + case 'T': if (pattern == "TZH") { + date_type_check(ExpectedDateType::TIMEZONE, desc, pattern, cb); + SSHORT timezoneOffset = extract_timezone_offset(desc); int timezoneSign = sign(timezoneOffset); SSHORT offsetInHours = abs(timezoneOffset / 60); @@ -1562,6 +1676,8 @@ static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::st } else if (pattern == "TZM") { + date_type_check(ExpectedDateType::TIMEZONE, desc, pattern, cb); + SSHORT timezoneOffset = extract_timezone_offset(desc); int timezoneSign = sign(timezoneOffset); SSHORT offsetInMinutes = abs(timezoneOffset % 60); @@ -1579,7 +1695,11 @@ static string datetime_to_format_string_pattern_matcher(const dsc* desc, std::st patternResult.printf(printfFormat.c_str(), offsetInMinutes); } else if (pattern == "TZR") + { + date_type_check(ExpectedDateType::TIMEZONE, desc, pattern, cb); + patternResult = extract_timezone_name(desc); + } break; default: @@ -1635,6 +1755,7 @@ string CVT_datetime_to_format_string(const dsc* desc, const string& format, Call } string formatUpper(format); + // Convert format to upper case except for text in double quotes for (int i = 0; i < formatUpper.length(); i++) { const char symbol = formatUpper[i]; @@ -1665,28 +1786,24 @@ string CVT_datetime_to_format_string(const dsc* desc, const string& format, Call break; } } + int formatLength = formatUpper.length(); string result; int formatOffset = 0; std::string_view pattern; std::string_view previousPattern; - for (int i = 0; i < formatUpper.length(); i++) + for (int i = 0; i < formatLength; i++) { const char symbol = formatUpper[i]; + // We meet separator, if it's double quotes - start copying this text into result string, if not, just insert separator 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; + int rawStringLength = formatLength - i; string rawString(rawStringLength, '\0'); for (int j = 0; j < rawStringLength; j++, i++) { @@ -1707,31 +1824,40 @@ string CVT_datetime_to_format_string(const dsc* desc, const string& format, Call 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++) + // Start reading format and comparing it to patterns, until we stop finding similarities + for (; i < formatLength; i++) { - if (!strncmp(TO_DATETIME_PATTERNS[j], pattern.data(), pattern.length())) + 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++) { - isFound = true; - if (i == formatUpper.length() - 1) + if (!strncmp(TO_DATETIME_PATTERNS[j], pattern.data(), pattern.length())) { - result += datetime_to_format_string_pattern_matcher(desc, pattern, previousPattern, times, - fractions, cb); + isFound = true; + if (i == formatLength - 1) + { + result += datetime_to_format_string_pattern_matcher(desc, pattern, previousPattern, times, + fractions, cb); + } + break; } - break; } + if (!isFound) + break; } - if (isFound) - continue; + + if (i == formatLength) + break; if (pattern.length() <= 1) invalid_pattern_exception(pattern, cb); + // Our current pattern contains real pattern + one extra symbol, so we need to drop it 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--; } @@ -1758,7 +1884,7 @@ static int roman_to_int(const char* str, int length, int& offset) case 'C': value = 100; break; case 'D': value = 500; break; case 'M': value = 1000; break; - default: return 0; + default: return result; } result += value; @@ -1799,17 +1925,17 @@ static int parse_string_to_get_int(const char* str, int length, int& offset, int } -static std::string_view parse_string_to_get_first_word(const char* str, int length, int& offset, int parseLength) +static std::string_view parse_string_to_get_substring(const char* str, int length, int& offset, int parseLength = 0, + bool onlyCharacters = true) { int wordLen = 0; int startPoint = offset; - const int parseLengthWithOffset = offset + parseLength; + const int parseLengthWithOffset = parseLength > 0 ? offset + parseLength : std::numeric_limits::max(); for (; offset < parseLengthWithOffset && offset < length; offset++) { - if (!LETTER(str[offset])) + if (onlyCharacters && !LETTER(str[offset])) break; - ++wordLen; } @@ -1817,7 +1943,52 @@ static std::string_view parse_string_to_get_first_word(const char* str, int leng } -static void string_to_format_datetime_pattern_matcher(std::string_view pattern, std::string_view previousPattern, +static Format::Patterns apply_period(std::string_view period, struct tm& outTimes, Firebird::Callbacks* cb) +{ + if (period == AM_PERIOD) + { + if (outTimes.tm_hour == 12) + outTimes.tm_hour = 0; + + return Format::AM; + } + else if (period == PM_PERIOD) + { + int hours = outTimes.tm_hour; + outTimes.tm_hour = hours == 12 ? hours : 12 + hours; + + return Format::PM; + } + + cb->err(Arg::Gds(isc_incorrect_hours_period) << string(period.data(), period.length())); +} + + +static int round_year_pattern_implementation(int parsedRRValue, int currentYear) +{ + int firstTwoDigits = currentYear / 100; + int lastTwoDigits = currentYear % 100; + + int result = 0; + + if (parsedRRValue < 50) + { + result = lastTwoDigits < 50 + ? firstTwoDigits * 100 + parsedRRValue + : (firstTwoDigits + 1) * 100 + parsedRRValue; + } + else + { + result = lastTwoDigits < 50 + ? (firstTwoDigits - 1) * 100 + parsedRRValue + : firstTwoDigits * 100 + parsedRRValue; + } + + return result; +} + + +static void string_to_format_datetime_pattern_matcher(std::string_view pattern, Format::Patterns& formatFlags, std::string_view previousPattern, const char* str, int strLength, int& strOffset, struct tm& outTimes, int& outFractions, SSHORT& outTimezoneInMinutes, USHORT& outTimezoneId, Firebird::Callbacks* cb) { @@ -1826,43 +1997,35 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, case 'Y': if (pattern == "Y") { + // Set last digit to zero + int currentYear = (outTimes.tm_year + 1900) / 10 * 10; int year = parse_string_to_get_int(str, strLength, strOffset, 1); - outTimes.tm_year = 2000 + year - 1900; + + outTimes.tm_year = currentYear + year - 1900; return; } else if (pattern == "YY") { - tm currentTm; - TimeStamp::getCurrentTimeStamp().decode(¤tTm); // Set 2 last digits to zero - int currentAge = (currentTm.tm_year + 1900) / 100 * 100; + int currentAge = (outTimes.tm_year + 1900) / 100 * 100; + int parsedYear = parse_string_to_get_int(str, strLength, strOffset, 2); - outTimes.tm_year = parse_string_to_get_int(str, strLength, strOffset, 2); - outTimes.tm_year += outTimes.tm_year < (currentTm.tm_year + 1900 - 50) % 100 - ? currentAge - : currentAge - 100; - - outTimes.tm_year -= 1900; + outTimes.tm_year = currentAge + parsedYear - 1900; return; } else if (pattern == "YYY") { - tm currentTm; - TimeStamp::getCurrentTimeStamp().decode(¤tTm); // Set 3 last digits to zero - int currentThousand = (currentTm.tm_year + 1900) / 1000 * 1000; + int currentThousand = (outTimes.tm_year + 1900) / 1000 * 1000; + int parsedYear = parse_string_to_get_int(str, strLength, strOffset, 3); - outTimes.tm_year = parse_string_to_get_int(str, strLength, strOffset, 3); - outTimes.tm_year += outTimes.tm_year < (currentTm.tm_year + 1900 - 500) % 1000 - ? currentThousand - : currentThousand - 1000; - - outTimes.tm_year -= 1900; + outTimes.tm_year = currentThousand + parsedYear - 1900; return; } else if (pattern == "YYYY") { int year = parse_string_to_get_int(str, strLength, strOffset, 4); + outTimes.tm_year = year - 1900; return; } @@ -1882,6 +2045,8 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, case 'M': if (pattern == "MI") { + formatFlags |= Format::MI; + int minutes = parse_string_to_get_int(str, strLength, strOffset, 2); if (minutes > 59) { @@ -1906,8 +2071,7 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, } else if (pattern == "MON") { - std::string_view monthShortName = parse_string_to_get_first_word(str, strLength, strOffset, 3); - int month = -1; + std::string_view monthShortName = parse_string_to_get_substring(str, strLength, strOffset, 3); for (int i = 0; i < FB_NELEM(FB_SHORT_MONTHS) - 1; i++) { if (std::equal(monthShortName.begin(), monthShortName.end(), @@ -1923,7 +2087,7 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, } else if (pattern == "MONTH") { - std::string_view monthFullName = parse_string_to_get_first_word(str, strLength, strOffset, strLength - strOffset); + std::string_view monthFullName = parse_string_to_get_substring(str, strLength, strOffset); for (int i = 0; i < FB_NELEM(FB_LONG_MONTHS_UPPER) - 1; i++) { if (std::equal(monthFullName.begin(), monthFullName.end(), @@ -1940,7 +2104,28 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, break; case 'R': - if (pattern == "RM") + if (pattern == "RR") + { + // tm_year already contains current date + int parsedYear = parse_string_to_get_int(str, strLength, strOffset, 2); + outTimes.tm_year = round_year_pattern_implementation(parsedYear, outTimes.tm_year + 1900) - 1900; + + return; + } + else if (pattern == "RRRR") + { + int startOffset = strOffset; + int parsedYear = parse_string_to_get_int(str, strLength, strOffset, 4); + int numberOfSymbols = strOffset - startOffset; + + outTimes.tm_year = numberOfSymbols <= 2 + ? round_year_pattern_implementation(parsedYear, outTimes.tm_year + 1900) + : parsedYear; + outTimes.tm_year -= 1900; + + return; + } + else if (pattern == "RM") { int month = roman_to_int(str, strLength, strOffset); if (month == 0 || month > 12) @@ -1995,32 +2180,22 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, if (pattern == "HH"|| pattern == "HH12") { + formatFlags |= Format::HH12; + int hours = parse_string_to_get_int(str, strLength, strOffset, 2); - if (hours > 12) + if (hours < 1 || 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)); + string(pattern.data(), pattern.length()) << Arg::Num(1) << 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())); + outTimes.tm_hour = hours; + return; } else if (pattern == "HH24") { + formatFlags |= Format::HH24; + int hours = parse_string_to_get_int(str, strLength, strOffset, 2); if (hours > 23) { @@ -2036,6 +2211,8 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, case 'S': if (pattern == "SS") { + formatFlags |= Format::SS; + int seconds = parse_string_to_get_int(str, strLength, strOffset, 2); if (seconds > 59) { @@ -2048,7 +2225,9 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, } else if (pattern == "SSSSS") { - constexpr int maximumSecondsInDay = 24 * 60 * 60 - 1; + formatFlags |= Format::SSSSS; + + constexpr int maximumSecondsInDay = NoThrowTimeStamp::SECONDS_PER_DAY - 1; int secondsInDay = parse_string_to_get_int(str, strLength, strOffset, 5); if (secondsInDay > maximumSecondsInDay) @@ -2083,9 +2262,37 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, } break; + case 'A': + if (pattern == AM_PERIOD) + { + // If we get A.M or P.M first, set flag, so we can calculate hours later + if (outTimes.tm_hour == 0) + formatFlags |= Format::AM_OR_PM_FIRST; + + std::string_view period = parse_string_to_get_substring(str, strLength, strOffset, sizeof(AM_PERIOD) - 1, false); + formatFlags |= apply_period(period, outTimes, cb); + return; + } + break; + + case 'P': + if (pattern == PM_PERIOD) + { + // If we get A.M or P.M first, set flag, so we can calculate hours later + if (outTimes.tm_hour == 0) + formatFlags |= Format::AM_OR_PM_FIRST; + + std::string_view period = parse_string_to_get_substring(str, strLength, strOffset, sizeof(PM_PERIOD) - 1, false); + formatFlags |= apply_period(period, outTimes, cb); + return; + } + break; + case 'T': if (pattern == "TZH") { + formatFlags |= Format::TZH; + if (previousPattern == "TZM") { outTimezoneInMinutes += sign(outTimezoneInMinutes) * @@ -2097,6 +2304,8 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, } else if (pattern == "TZM") { + formatFlags |= Format::TZM; + if (previousPattern == "TZH") { outTimezoneInMinutes += sign(outTimezoneInMinutes) * @@ -2108,8 +2317,6 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, } else if (pattern == "TZR") { - USHORT timezoneId; - int parsedTimezoneNameLength; bool timezoneNameIsCorrect = timeZoneTrie().contains(str + strOffset, outTimezoneId, parsedTimezoneNameLength); if (!timezoneNameIsCorrect) @@ -2124,9 +2331,49 @@ static void string_to_format_datetime_pattern_matcher(std::string_view pattern, invalid_pattern_exception(pattern, cb); } +// These rules are taken from ISO/IEC 9075-2:2023(E) 9.52 Datetime templates +static void validate_format_flags(Format::Patterns formatFlags, Callbacks* cb) +{ + if (formatFlags & (Format::HH12 | Format::AM | Format::PM)) + { + // If CT contains , then CT shall not contain or . + if (formatFlags & Format::HH24) + cb->err(Arg::Gds(isc_incompatible_format_patterns) << Arg::Str("HH24") << Arg::Str("HH/HH12/A.M./P.M.")); -ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird::string& format, Firebird::Callbacks* cb, - const EXPECT_DATETIME expectedType) + // If CT contains , then CT shall contain and shall not contain . + // If CT contains , then CT shall contain and shall not contain . + if (!(formatFlags & Format::HH12) == (formatFlags & Format::AM || formatFlags & Format::PM)) + { + cb->err(Arg::Gds(isc_pattern_cant_be_used_without_other_pattern_and_vice_versa) + << Arg::Str("HH/HH12") << Arg::Str("A.M./P.M.")); + } + } + + // If CT contains , then CT shall not contain any of the following: + // , , , , or . + if (formatFlags & Format::SSSSS) + { + if (formatFlags & Format::HH12) + cb->err(Arg::Gds(isc_incompatible_format_patterns) << Arg::Str("SSSSS") << Arg::Str("HH/HH12")); + if (formatFlags & Format::HH24) + cb->err(Arg::Gds(isc_incompatible_format_patterns) << Arg::Str("SSSSS") << Arg::Str("HH24")); + if (formatFlags & Format::MI) + cb->err(Arg::Gds(isc_incompatible_format_patterns) << Arg::Str("SSSSS") << Arg::Str("MI")); + if (formatFlags & Format::SS) + cb->err(Arg::Gds(isc_incompatible_format_patterns) << Arg::Str("SSSSS") << Arg::Str("HH/HH12")); + if (formatFlags & (Format::AM | Format::PM)) + cb->err(Arg::Gds(isc_incompatible_format_patterns) << Arg::Str("SSSSS") << Arg::Str("A.M./P.M.")); + } + + // If CT contains , then CT shall contain . + if (formatFlags & Format::TZM && !(formatFlags & Format::TZH)) + cb->err(Arg::Gds(isc_pattern_cant_be_used_without_other_pattern) << Arg::Str("TZM") << Arg::Str("TZH")); +} + + +ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird::string& format, + const EXPECT_DATETIME expectedType, Firebird::Callbacks* cb) { if (!DTYPE_IS_TEXT(desc->dsc_dtype)) cb->err(Arg::Gds(isc_invalid_data_type_for_date_format)); @@ -2145,11 +2392,12 @@ ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird:: string formatUpper(format.length(), '\0'); for (int i = 0; i < format.length(); i++) formatUpper[i] = toupper(format[i]); + int formatLength = formatUpper.length(); + struct tm times; - memset(×, 0, sizeof(struct tm)); - times.tm_year = 1 - 1900; - times.tm_mday = 1; + memset(×, 0, sizeof(times)); + NoThrowTimeStamp::decode_date(cb->getLocalDate(), ×); int fractions = 0; @@ -2163,22 +2411,19 @@ ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird:: std::string_view pattern; std::string_view previousPattern; - for (int i = 0; i < formatUpper.length(); i++) + Format::Patterns formatFlags = Format::NONE; + + for (int i = 0; i < formatLength; i++) { - const char symbol = formatUpper[i]; - - if (is_separator(symbol)) + // Iterate through format and string until we meet any non separator symbol + for (; i < formatLength; i++, formatOffset++) { - 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; + if (!is_separator(formatUpper[i])) + break; } + // All remaining characters were separators, so we have fully read the format, get out + if (i == formatLength) + break; for (; stringOffset < stringLength; stringOffset++) { @@ -2189,31 +2434,41 @@ ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird:: if (stringOffset >= stringLength) cb->err(Arg::Gds(isc_data_for_format_is_exhausted) << string(formatUpper.c_str() + formatOffset)); - 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++) + // Start reading format and comparing it to patterns, until we stop finding similarities + for (; i < formatLength; i++) { - if (!strncmp(TO_STRING_PATTERNS[j], pattern.data(), pattern.length())) + 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++) { - isFound = true; - if (i == formatUpper.length() - 1) + if (!strncmp(TO_STRING_PATTERNS[j], pattern.data(), pattern.length())) { - string_to_format_datetime_pattern_matcher(pattern, previousPattern, stringUpper.c_str(), - stringLength, stringOffset, times, fractions, timezoneOffsetInMinutes, timezoneId, cb); + isFound = true; + if (i == formatLength - 1) + { + string_to_format_datetime_pattern_matcher(pattern, formatFlags, previousPattern, stringUpper.c_str(), + stringLength, stringOffset, times, fractions, timezoneOffsetInMinutes, timezoneId, cb); + } + break; } - break; } + if (!isFound) + break; } - if (isFound) - continue; + + if (i == formatLength) + break; if (pattern.length() <= 1) invalid_pattern_exception(pattern, cb); + // Our current pattern contains real pattern + one extra symbol, so we need to drop it pattern = pattern.substr(0, pattern.length() - 1); - string_to_format_datetime_pattern_matcher(pattern, previousPattern, stringUpper.c_str(), + string_to_format_datetime_pattern_matcher(pattern, formatFlags, previousPattern, stringUpper.c_str(), stringLength, stringOffset, times, fractions, timezoneOffsetInMinutes, timezoneId, cb); previousPattern = pattern; + formatOffset = i; i--; } @@ -2227,6 +2482,16 @@ ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird:: if (stringOffset < stringLength) cb->err(Arg::Gds(isc_trailing_part_of_string) << string(stringUpper.c_str() + stringOffset)); + + validate_format_flags(formatFlags, cb); + + // Deferred application of 12h period, if period was encountered first + if (formatFlags & Format::AM_OR_PM_FIRST) + { + bool periodIsAm = static_cast(formatFlags & Format::AM); + apply_period(periodIsAm ? AM_PERIOD : PM_PERIOD, times, cb); + } + ISC_TIMESTAMP_TZ timestampTZ; timestampTZ.utc_timestamp = NoThrowTimeStamp::encode_timestamp(×, fractions); @@ -2242,21 +2507,9 @@ ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird:: abs(timezoneOffsetInMinutes) / 60, abs(timezoneOffsetInMinutes) % 60); } - if (expectedType == expect_sql_time_tz || expectedType == expect_timestamp_tz || timestampTZ.time_zone != sessionTimeZone) - TimeZoneUtil::localTimeStampToUtc(timestampTZ); + timeStampToUtc(timestampTZ, sessionTimeZone, expectedType, cb); - if (timestampTZ.time_zone != sessionTimeZone) - { - if (expectedType == expect_sql_time) - { - ISC_TIME_TZ timeTz; - timeTz.utc_time = timestampTZ.utc_timestamp.timestamp_time; - timeTz.time_zone = timestampTZ.time_zone; - timestampTZ.utc_timestamp.timestamp_time = TimeZoneUtil::timeTzToTime(timeTz, cb); - } - else if (expectedType == expect_timestamp) - *(ISC_TIMESTAMP*) ×tampTZ = TimeZoneUtil::timeStampTzToTimeStamp(timestampTZ, sessionTimeZone); - } + validateTimeStamp(timestampTZ.utc_timestamp, expectedType, desc, cb); return timestampTZ; } diff --git a/src/common/cvt.h b/src/common/cvt.h index 2fd0ecedfa..aaf6eed9f3 100644 --- a/src/common/cvt.h +++ b/src/common/cvt.h @@ -106,7 +106,7 @@ void CVT_string_to_datetime(const dsc*, ISC_TIMESTAMP_TZ*, bool*, const Firebird 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, - const Firebird::EXPECT_DATETIME expectedType); +ISC_TIMESTAMP_TZ CVT_string_to_format_datetime(const dsc* desc, const Firebird::string& format, + const Firebird::EXPECT_DATETIME expectedType, Firebird::Callbacks* cb); #endif //COMMON_CVT_H diff --git a/src/common/tests/CvtTest.cpp b/src/common/tests/CvtTest.cpp index b28e14e1d5..c2893266b6 100644 --- a/src/common/tests/CvtTest.cpp +++ b/src/common/tests/CvtTest.cpp @@ -6,29 +6,42 @@ using namespace Firebird; using namespace Jrd; using namespace CvtTestUtils; + BOOST_AUTO_TEST_SUITE(CVTSuite) BOOST_AUTO_TEST_SUITE(CVTDatetimeFormat) +// Currently we cannot print our error messages because we need master interface for that static void errFunc(const Firebird::Arg::StatusVector& v) { v.raise(); } -CVTCallback cb(errFunc); +MockCallback cb(errFunc, std::bind(mockGetLocalDate, 2023)); 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; + try + { + 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_TEST_INFO("FORMAT: " << "\"" << format.c_str() << "\""); + + string result = CVT_datetime_to_format_string(&desc, format, &cb); + + BOOST_TEST(result == expected, "\nRESULT: " << result.c_str() << "\nEXPECTED: " << expected.c_str()); + } + catch(const Exception& ex) + { + BOOST_TEST_INFO("Exception was caught!"); + BOOST_TEST(false); + } } BOOST_AUTO_TEST_SUITE(FunctionalTest) @@ -44,9 +57,56 @@ BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_DATE) 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, 2, 1), "Q", "1", cb); + testCVTDatetimeToFormatString(createDate(1, 3, 1), "Q", "1", cb); testCVTDatetimeToFormatString(createDate(1, 4, 1), "Q", "2", cb); + testCVTDatetimeToFormatString(createDate(1, 5, 1), "Q", "2", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 1), "Q", "2", cb); testCVTDatetimeToFormatString(createDate(1, 7, 1), "Q", "3", cb); + testCVTDatetimeToFormatString(createDate(1, 8, 1), "Q", "3", cb); + testCVTDatetimeToFormatString(createDate(1, 9, 1), "Q", "3", cb); testCVTDatetimeToFormatString(createDate(1, 10, 1), "Q", "4", cb); + testCVTDatetimeToFormatString(createDate(1, 11, 1), "Q", "4", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 1), "Q", "4", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "MON", "Jan", cb); + testCVTDatetimeToFormatString(createDate(1, 2, 1), "MON", "Feb", cb); + testCVTDatetimeToFormatString(createDate(1, 3, 1), "MON", "Mar", cb); + testCVTDatetimeToFormatString(createDate(1, 4, 1), "MON", "Apr", cb); + testCVTDatetimeToFormatString(createDate(1, 5, 1), "MON", "May", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 1), "MON", "Jun", cb); + testCVTDatetimeToFormatString(createDate(1, 7, 1), "MON", "Jul", cb); + testCVTDatetimeToFormatString(createDate(1, 8, 1), "MON", "Aug", cb); + testCVTDatetimeToFormatString(createDate(1, 9, 1), "MON", "Sep", cb); + testCVTDatetimeToFormatString(createDate(1, 10, 1), "MON", "Oct", cb); + testCVTDatetimeToFormatString(createDate(1, 11, 1), "MON", "Nov", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 1), "MON", "Dec", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "MONTH", "JANUARY", cb); + testCVTDatetimeToFormatString(createDate(1, 2, 1), "MONTH", "FEBRUARY", cb); + testCVTDatetimeToFormatString(createDate(1, 3, 1), "MONTH", "MARCH", cb); + testCVTDatetimeToFormatString(createDate(1, 4, 1), "MONTH", "APRIL", cb); + testCVTDatetimeToFormatString(createDate(1, 5, 1), "MONTH", "MAY", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 1), "MONTH", "JUNE", cb); + testCVTDatetimeToFormatString(createDate(1, 7, 1), "MONTH", "JULY", cb); + testCVTDatetimeToFormatString(createDate(1, 8, 1), "MONTH", "AUGUST", cb); + testCVTDatetimeToFormatString(createDate(1, 9, 1), "MONTH", "SEPTEMBER", cb); + testCVTDatetimeToFormatString(createDate(1, 10, 1), "MONTH", "OCTOBER", cb); + testCVTDatetimeToFormatString(createDate(1, 11, 1), "MONTH", "NOVEMBER", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 1), "MONTH", "DECEMBER", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "RM", "I", cb); + testCVTDatetimeToFormatString(createDate(1, 2, 1), "RM", "II", cb); + testCVTDatetimeToFormatString(createDate(1, 3, 1), "RM", "III", cb); + testCVTDatetimeToFormatString(createDate(1, 4, 1), "RM", "IV", cb); + testCVTDatetimeToFormatString(createDate(1, 5, 1), "RM", "V", cb); + testCVTDatetimeToFormatString(createDate(1, 6, 1), "RM", "VI", cb); + testCVTDatetimeToFormatString(createDate(1, 7, 1), "RM", "VII", cb); + testCVTDatetimeToFormatString(createDate(1, 8, 1), "RM", "VIII", cb); + testCVTDatetimeToFormatString(createDate(1, 9, 1), "RM", "IX", cb); + testCVTDatetimeToFormatString(createDate(1, 10, 1), "RM", "X", cb); + testCVTDatetimeToFormatString(createDate(1, 11, 1), "RM", "XI", cb); + testCVTDatetimeToFormatString(createDate(1, 12, 1), "RM", "XII", 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); @@ -56,6 +116,22 @@ BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_DATE) testCVTDatetimeToFormatString(createDate(1, 6, 15), "WW-W", "24-3", cb); testCVTDatetimeToFormatString(createDate(1, 12, 30), "WW.W", "52.5", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 1), "DAY", "MONDAY", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 2), "DAY", "TUESDAY", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 3), "DAY", "WEDNESDAY", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 4), "DAY", "THURSDAY", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 5), "DAY", "FRIDAY", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 6), "DAY", "SATURDAY", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 7), "DAY", "SUNDAY", cb); + + testCVTDatetimeToFormatString(createDate(1, 1, 1), "DY", "Mon", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 2), "DY", "Tue", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 3), "DY", "Wed", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 4), "DY", "Thu", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 5), "DY", "Fri", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 6), "DY", "Sat", cb); + testCVTDatetimeToFormatString(createDate(1, 1, 7), "DY", "Sun", 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); @@ -72,9 +148,47 @@ BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_DATE) 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), "HH24", "00", cb); + testCVTDatetimeToFormatString(createTime(12, 0, 0), "HH24", "12", cb); + testCVTDatetimeToFormatString(createTime(23, 0, 0), "HH24", "23", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0), "HH A.M.", "12 A.M.", cb); + testCVTDatetimeToFormatString(createTime(0, 0, 0), "HH P.M.", "12 A.M.", cb); + testCVTDatetimeToFormatString(createTime(11, 0, 0), "HH A.M.", "11 A.M.", cb); + testCVTDatetimeToFormatString(createTime(11, 0, 0), "HH P.M.", "11 A.M.", cb); + testCVTDatetimeToFormatString(createTime(12, 0, 0), "HH A.M.", "12 P.M.", cb); + testCVTDatetimeToFormatString(createTime(12, 0, 0), "HH P.M.", "12 P.M.", cb); + testCVTDatetimeToFormatString(createTime(13, 0, 0), "HH A.M.", "01 P.M.", cb); + testCVTDatetimeToFormatString(createTime(13, 0, 0), "HH P.M.", "01 P.M.", cb); + testCVTDatetimeToFormatString(createTime(23, 0, 0), "HH A.M.", "11 P.M.", cb); + testCVTDatetimeToFormatString(createTime(23, 0, 0), "HH P.M.", "11 P.M.", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0), "HH12 A.M.", "12 A.M.", cb); + testCVTDatetimeToFormatString(createTime(0, 0, 0), "HH12 P.M.", "12 A.M.", cb); + testCVTDatetimeToFormatString(createTime(11, 0, 0), "HH12 A.M.", "11 A.M.", cb); + testCVTDatetimeToFormatString(createTime(11, 0, 0), "HH12 P.M.", "11 A.M.", cb); + testCVTDatetimeToFormatString(createTime(12, 0, 0), "HH12 A.M.", "12 P.M.", cb); + testCVTDatetimeToFormatString(createTime(12, 0, 0), "HH12 P.M.", "12 P.M.", cb); + testCVTDatetimeToFormatString(createTime(13, 0, 0), "HH12 A.M.", "01 P.M.", cb); + testCVTDatetimeToFormatString(createTime(13, 0, 0), "HH12 P.M.", "01 P.M.", cb); + testCVTDatetimeToFormatString(createTime(23, 0, 0), "HH12 A.M.", "11 P.M.", cb); + testCVTDatetimeToFormatString(createTime(23, 0, 0), "HH12 P.M.", "11 P.M.", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0), "MI", "00", cb); + testCVTDatetimeToFormatString(createTime(0, 30, 0), "MI", "30", cb); + testCVTDatetimeToFormatString(createTime(0, 59, 0), "MI", "59", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0), "SS", "00", cb); + testCVTDatetimeToFormatString(createTime(0, 0, 30), "SS", "30", cb); + testCVTDatetimeToFormatString(createTime(0, 0, 59), "SS", "59", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0), "SSSSS", "0", cb); + testCVTDatetimeToFormatString(createTime(12, 30, 15), "SSSSS", "45015", cb); + testCVTDatetimeToFormatString(createTime(23, 59, 59), "SSSSS", "86399", cb); + + testCVTDatetimeToFormatString(createTime(0, 0, 0), "HH-HH12 A.M..HH24,MI/SS SSSSS", "12-12 A.M..00,00/00 0", cb); + testCVTDatetimeToFormatString(createTime(12, 35, 15), "HH.HH12 P.M.:HH24;MI-SS/SSSSS", "12.12 P.M.:12;35-15/45315", cb); + testCVTDatetimeToFormatString(createTime(23, 59, 59), " HH P.M. - HH12 . HH24 , MI / SS SSSSS ", " 11 P.M. - 11 . 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); @@ -88,12 +202,12 @@ BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_TIMESTAMP) 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); + testCVTDatetimeToFormatString(timestamp, "HH-HH12 P.M.-HH24-MI-SS-SSSSS.FF2", "01-01 A.M.-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, 0, 900), "HH A.M.-HH12-HH24-MI-SS-SSSSS.FF1/TZH/TZM", "03 P.M.-03-15-35-59-56159.9/+00/00", cb); testCVTDatetimeToFormatString(createTimeTZ(15, 35, 59, 160), "HH24:MI-TZH:TZM", "15:35-+02:40", cb); testCVTDatetimeToFormatString(createTimeTZ(15, 35, 59, -160), "HH24:MI TZH:TZM", "15:35 -02:40", cb); @@ -109,7 +223,7 @@ BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_TIMESTAMP_TZ) 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(timestampTZ, "HH A.M.-HH12-HH24-MI-SS-SSSSS.FF2/TZH/TZM", "01 A.M.-01-01-34-15-5655.50/+00/00", cb); testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 1, 34, 15, 70), "HH24:MI-TZH:TZM", "01:34-+01:10", cb); testCVTDatetimeToFormatString(createTimeStampTZ(1982, 4, 21, 1, 34, 15, -70), "HH24:MI TZH:TZM", "01:34 -01:10", cb); @@ -126,7 +240,7 @@ BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_SOLID_PATTERNS) 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); + testCVTDatetimeToFormatString(timestampTZ, "HHHH12A.M.HH24MISSSSSSSFF2TZHTZM", "0101A.M.013456551550+0000", cb); } BOOST_AUTO_TEST_CASE(CVTDatetimeToFormatStringTest_RAW_TEXT) @@ -149,37 +263,48 @@ BOOST_AUTO_TEST_SUITE(CVTStringToFormatDateTime) static void testCVTStringToFormatDateTime(const string& date, const string& format, const ISC_TIMESTAMP_TZ& expected, Firebird::EXPECT_DATETIME expectedType, Callbacks& cb) { - string varyingString = "xx"; - varyingString += date; - *(USHORT*) varyingString.data() = varyingString.size() - sizeof(USHORT); + try + { + 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; + 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, expectedType); + BOOST_TEST_INFO("INPUT: " << "\"" << date.c_str() << "\""); + BOOST_TEST_INFO("FORMAT: " << "\"" << format.c_str() << "\""); - struct tm resultTimes; - memset(&resultTimes, 0, sizeof(resultTimes)); - int resultFractions; - NoThrowTimeStamp::decode_timestamp(result.utc_timestamp, &resultTimes, &resultFractions); - SSHORT resultOffset; - TimeZoneUtil::extractOffset(result, &resultOffset); + const ISC_TIMESTAMP_TZ result = CVT_string_to_format_datetime(&desc, format, expectedType, &cb); - struct tm expectedTimes; - memset(&expectedTimes, 0, sizeof(expectedTimes)); - int expectedFractions; - NoThrowTimeStamp::decode_timestamp(expected.utc_timestamp, &expectedTimes, &expectedFractions); - SSHORT expectedOffset; - TimeZoneUtil::extractOffset(expected, &expectedOffset); + struct tm resultTimes; + memset(&resultTimes, 0, sizeof(resultTimes)); + int resultFractions; + NoThrowTimeStamp::decode_timestamp(result.utc_timestamp, &resultTimes, &resultFractions); + SSHORT resultOffset; + TimeZoneUtil::extractOffset(result, &resultOffset); - bool isEqual = !((bool) memcmp(&resultTimes, &expectedTimes, sizeof(struct tm))) - && resultFractions == expectedFractions && resultOffset == expectedOffset; + struct tm expectedTimes; + memset(&expectedTimes, 0, sizeof(expectedTimes)); + int expectedFractions; + NoThrowTimeStamp::decode_timestamp(expected.utc_timestamp, &expectedTimes, &expectedFractions); + SSHORT expectedOffset; + TimeZoneUtil::extractOffset(expected, &expectedOffset); - BOOST_TEST(isEqual, "\nRESULT: " << DECOMPOSE_TM_STRUCT(resultTimes, resultFractions, resultOffset) - << "\nEXPECTED: " << DECOMPOSE_TM_STRUCT(expectedTimes, expectedFractions, 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)); + } + catch(const Exception& ex) + { + BOOST_TEST_INFO("Exception was caught!"); + BOOST_TEST(false); + } } static void testCVTStringToFormatDateTimeExpectDate(const string& date, const string& format, @@ -200,48 +325,141 @@ static void testCVTStringToFormatDateTimeExpectTimeTZ(const string& date, const testCVTStringToFormatDateTime(date, format, expected, expect_sql_time_tz, cb); }; +static void testExceptionCvtStringToFormatDateTime(const string& date, const string& format, 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; + + BOOST_TEST_INFO("INPUT: " << "\"" << date.c_str() << "\""); + BOOST_TEST_INFO("FORMAT: " << "\"" << format.c_str() << "\""); + + BOOST_CHECK_THROW(CVT_string_to_format_datetime(&desc, format, expect_timestamp_tz, &cb), status_exception); +} + BOOST_AUTO_TEST_SUITE(FunctionalTest) BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_DATE) { - testCVTStringToFormatDateTimeExpectDate("1", "YEAR", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1234", "YEAR", createTimeStampTZ(1234, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("9999", "YEAR", createTimeStampTZ(9999, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1", "YEAR", createTimeStampTZ(1, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("0001", "YEAR", createTimeStampTZ(1, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1234", "YEAR", createTimeStampTZ(1234, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("9999", "YEAR", createTimeStampTZ(9999, 0, 0, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1", "YYYY", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1234", "YYYY", createTimeStampTZ(1234, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("9999", "YYYY", createTimeStampTZ(9999, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1", "YYYY", createTimeStampTZ(1, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("0001", "YYYY", createTimeStampTZ(1, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1234", "YYYY", createTimeStampTZ(1234, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("9999", "YYYY", createTimeStampTZ(9999, 0, 0, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1", "YYY", createTimeStampTZ(2001, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("522", "YYY", createTimeStampTZ(2522, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("999", "YYY", createTimeStampTZ(1999, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1", "YYY", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("001", "YYY", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("522", "YYY", createTimeStampTZ(2522, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("523", "YYY", createTimeStampTZ(2523, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("999", "YYY", createTimeStampTZ(2999, 0, 0, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1", "YY", createTimeStampTZ(2001, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("72", "YY", createTimeStampTZ(2072, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("99", "YY", createTimeStampTZ(1999, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1", "YY", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("01", "YY", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("50", "YY", createTimeStampTZ(2050, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("99", "YY", createTimeStampTZ(2099, 0, 0, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1", "Y", createTimeStampTZ(2001, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("9", "Y", createTimeStampTZ(2009, 1, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1", "Y", createTimeStampTZ(2021, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("9", "Y", createTimeStampTZ(2029, 0, 0, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("1", "MM", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("6", "MM", createTimeStampTZ(1, 6, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("12", "MM", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + { + // RR pattern depends on last 2 digits of current year + MockCallback cb_2000(errFunc, std::bind(mockGetLocalDate, 2000)); + MockCallback cb_2049(errFunc, std::bind(mockGetLocalDate, 2049)); + MockCallback cb_2050(errFunc, std::bind(mockGetLocalDate, 2050)); + MockCallback cb_2099(errFunc, std::bind(mockGetLocalDate, 2099)); - testCVTStringToFormatDateTimeExpectDate("Jan", "MON", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("Jun", "MON", createTimeStampTZ(1, 6, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("Dec", "MON", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1", "RR", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("1", "RR", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb_2049); + testCVTStringToFormatDateTimeExpectDate("1", "RR", createTimeStampTZ(2101, 0, 0, 0, 0, 0, 0), cb_2050); + testCVTStringToFormatDateTimeExpectDate("1", "RR", createTimeStampTZ(2101, 0, 0, 0, 0, 0, 0), cb_2099); - testCVTStringToFormatDateTimeExpectDate("January", "MONTH", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("June", "MONTH", createTimeStampTZ(1, 6, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("December", "MONTH", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("01", "RR", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("01", "RR", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb_2049); + testCVTStringToFormatDateTimeExpectDate("01", "RR", createTimeStampTZ(2101, 0, 0, 0, 0, 0, 0), cb_2050); + testCVTStringToFormatDateTimeExpectDate("01", "RR", createTimeStampTZ(2101, 0, 0, 0, 0, 0, 0), cb_2099); - testCVTStringToFormatDateTimeExpectDate("I", "RM", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("IV", "RM", createTimeStampTZ(1, 4, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("XII", "RM", createTimeStampTZ(1, 12, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("49", "RR", createTimeStampTZ(2049, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("49", "RR", createTimeStampTZ(2049, 0, 0, 0, 0, 0, 0), cb_2049); + testCVTStringToFormatDateTimeExpectDate("49", "RR", createTimeStampTZ(2149, 0, 0, 0, 0, 0, 0), cb_2050); + testCVTStringToFormatDateTimeExpectDate("49", "RR", createTimeStampTZ(2149, 0, 0, 0, 0, 0, 0), cb_2099); - testCVTStringToFormatDateTimeExpectDate("1", "DD", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("15", "DD", createTimeStampTZ(1, 1, 15, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectDate("31", "DD", createTimeStampTZ(1, 1, 31, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("50", "RR", createTimeStampTZ(1950, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("50", "RR", createTimeStampTZ(1950, 0, 0, 0, 0, 0, 0), cb_2049); + testCVTStringToFormatDateTimeExpectDate("50", "RR", createTimeStampTZ(2050, 0, 0, 0, 0, 0, 0), cb_2050); + testCVTStringToFormatDateTimeExpectDate("50", "RR", createTimeStampTZ(2050, 0, 0, 0, 0, 0, 0), cb_2099); + + testCVTStringToFormatDateTimeExpectDate("99", "RR", createTimeStampTZ(1999, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("99", "RR", createTimeStampTZ(1999, 0, 0, 0, 0, 0, 0), cb_2049); + testCVTStringToFormatDateTimeExpectDate("99", "RR", createTimeStampTZ(2099, 0, 0, 0, 0, 0, 0), cb_2050); + testCVTStringToFormatDateTimeExpectDate("99", "RR", createTimeStampTZ(2099, 0, 0, 0, 0, 0, 0), cb_2099); + + testCVTStringToFormatDateTimeExpectDate("1", "RRRR", createTimeStampTZ(2001, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("99", "RRRR", createTimeStampTZ(1999, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("0001", "RRRR", createTimeStampTZ(1, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("0099", "RRRR", createTimeStampTZ(99, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("1000", "RRRR", createTimeStampTZ(1000, 0, 0, 0, 0, 0, 0), cb_2000); + testCVTStringToFormatDateTimeExpectDate("9999", "RRRR", createTimeStampTZ(9999, 0, 0, 0, 0, 0, 0), cb_2000); + } + + testCVTStringToFormatDateTimeExpectDate("1", "MM", createTimeStampTZ(0, 1, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("01", "MM", createTimeStampTZ(0, 1, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("6", "MM", createTimeStampTZ(0, 6, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("06", "MM", createTimeStampTZ(0, 6, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("12", "MM", createTimeStampTZ(0, 12, 0, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTimeExpectDate("Jan", "MON", createTimeStampTZ(0, 1, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Feb", "MON", createTimeStampTZ(0, 2, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Mar", "MON", createTimeStampTZ(0, 3, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Apr", "MON", createTimeStampTZ(0, 4, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("May", "MON", createTimeStampTZ(0, 5, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Jun", "MON", createTimeStampTZ(0, 6, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Jul", "MON", createTimeStampTZ(0, 7, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Aug", "MON", createTimeStampTZ(0, 8, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Sep", "MON", createTimeStampTZ(0, 9, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Oct", "MON", createTimeStampTZ(0, 10, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Nov", "MON", createTimeStampTZ(0, 11, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("Dec", "MON", createTimeStampTZ(0, 12, 0, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTimeExpectDate("January", "MONTH", createTimeStampTZ(0, 1, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("February", "MONTH", createTimeStampTZ(0, 2, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("March", "MONTH", createTimeStampTZ(0, 3, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("April", "MONTH", createTimeStampTZ(0, 4, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("May", "MONTH", createTimeStampTZ(0, 5, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("June", "MONTH", createTimeStampTZ(0, 6, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("July", "MONTH", createTimeStampTZ(0, 7, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("August", "MONTH", createTimeStampTZ(0, 8, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("September", "MONTH", createTimeStampTZ(0, 9, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("October", "MONTH", createTimeStampTZ(0, 10, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("November", "MONTH", createTimeStampTZ(0, 11, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("December", "MONTH", createTimeStampTZ(0, 12, 0, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTimeExpectDate("I", "RM", createTimeStampTZ(0, 1, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("II", "RM", createTimeStampTZ(0, 2, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("III", "RM", createTimeStampTZ(0, 3, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("IV", "RM", createTimeStampTZ(0, 4, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("V", "RM", createTimeStampTZ(0, 5, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("VI", "RM", createTimeStampTZ(0, 6, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("VII", "RM", createTimeStampTZ(0, 7, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("VIII", "RM", createTimeStampTZ(0, 8, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("IX", "RM", createTimeStampTZ(0, 9, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("X", "RM", createTimeStampTZ(0, 10, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("XI", "RM", createTimeStampTZ(0, 11, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("XII", "RM", createTimeStampTZ(0, 12, 0, 0, 0, 0, 0), cb); + + testCVTStringToFormatDateTimeExpectDate("1", "DD", createTimeStampTZ(0, 0, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("01", "DD", createTimeStampTZ(0, 0, 1, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("15", "DD", createTimeStampTZ(0, 0, 15, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("31", "DD", createTimeStampTZ(0, 0, 31, 0, 0, 0, 0), cb); testCVTStringToFormatDateTimeExpectDate("2451887", "J", createTimeStampTZ(2000, 12, 8, 0, 0, 0, 0), cb); testCVTStringToFormatDateTimeExpectDate("1721426", "J", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); @@ -251,85 +469,150 @@ BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_DATE) testCVTStringToFormatDateTimeExpectDate("1981-8/13", "YEAR.MM.DD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); testCVTStringToFormatDateTimeExpectDate("9999 12;31", "YEAR.MM.DD", createTimeStampTZ(9999, 12, 31, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1981-Aug/13", "YEAR.MON.DD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1981-August/13", "YEAR.MONTH.DD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("1981-VIII/13", "YEAR.RM.DD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectDate("25.Jan.25", "YY;MON;DD", createTimeStampTZ(2025, 1, 25, 0, 0, 0, 0), cb); testCVTStringToFormatDateTimeExpectDate("./.1981./-8--/13--", " YEAR. -.MM.,,-.DD//", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); } BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_TIME) { - testCVTStringToFormatDateTimeExpectTime("12 AM", "HH", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("1 AM", "HH", createTimeStampTZ(1, 1, 1, 1, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("11 AM", "HH", createTimeStampTZ(1, 1, 1, 11, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 A.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 A.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 A.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 A.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 A.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 A.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 A.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 11, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 A.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 11, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("12 PM", "HH", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("1 PM", "HH", createTimeStampTZ(1, 1, 1, 13, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("11 PM", "HH", createTimeStampTZ(1, 1, 1, 23, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 P.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 P.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 P.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 P.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 P.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 P.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 P.M.", "HH A.M.", createTimeStampTZ(0, 0, 0, 23, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 P.M.", "HH P.M.", createTimeStampTZ(0, 0, 0, 23, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("12 AM", "HH12", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("1 AM", "HH12", createTimeStampTZ(1, 1, 1, 1, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("11 AM", "HH12", createTimeStampTZ(1, 1, 1, 11, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 A.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 A.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 A.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 A.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 A.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 A.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 A.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 11, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 A.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 11, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("12 PM", "HH12", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("1 PM", "HH12", createTimeStampTZ(1, 1, 1, 13, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("11 PM", "HH12", createTimeStampTZ(1, 1, 1, 23, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 P.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12 P.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 P.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 P.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("1 P.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("01 P.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 P.M.", "HH12 A.M.", createTimeStampTZ(0, 0, 0, 23, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("11 P.M.", "HH12 P.M.", createTimeStampTZ(0, 0, 0, 23, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("0", "HH24", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("12", "HH24", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("23", "HH24", createTimeStampTZ(1, 1, 1, 23, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("A.M. 12", "A.M. HH", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("A.M. 1", "P.M. HH", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("A.M. 01", "P.M. HH", createTimeStampTZ(0, 0, 0, 1, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("A.M. 11", "A.M. HH", createTimeStampTZ(0, 0, 0, 11, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("P.M. 12", "A.M. HH", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("P.M. 1", "P.M. HH", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("P.M. 01", "P.M. HH", createTimeStampTZ(0, 0, 0, 13, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("P.M. 11", "A.M. HH", createTimeStampTZ(0, 0, 0, 23, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("0", "MI", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("30", "MI", createTimeStampTZ(1, 1, 1, 0, 30, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("59", "MI", createTimeStampTZ(1, 1, 1, 0, 59, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("0", "HH24", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("00", "HH24", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("12", "HH24", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("23", "HH24", createTimeStampTZ(0, 0, 0, 23, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("0", "SS", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("30", "SS", createTimeStampTZ(1, 1, 1, 0, 0, 30, 0), cb); - testCVTStringToFormatDateTimeExpectTime("59", "SS", createTimeStampTZ(1, 1, 1, 0, 0, 59, 0), cb); + testCVTStringToFormatDateTimeExpectTime("0", "MI", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("00", "MI", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("30", "MI", createTimeStampTZ(0, 0, 0, 0, 30, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("59", "MI", createTimeStampTZ(0, 0, 0, 0, 59, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("0", "SSSSS", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0), cb); - testCVTStringToFormatDateTimeExpectTime("45315", "SSSSS", createTimeStampTZ(1, 1, 1, 12, 35, 15, 0), cb); - testCVTStringToFormatDateTimeExpectTime("86399", "SSSSS", createTimeStampTZ(1, 1, 1, 23, 59, 59, 0), cb); + testCVTStringToFormatDateTimeExpectTime("0", "SS", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("00", "SS", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("30", "SS", createTimeStampTZ(0, 0, 0, 0, 0, 30, 0), cb); + testCVTStringToFormatDateTimeExpectTime("59", "SS", createTimeStampTZ(0, 0, 0, 0, 0, 59, 0), cb); - testCVTStringToFormatDateTimeExpectTime("1", "FF1", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); - testCVTStringToFormatDateTimeExpectTime("5", "FF1", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); - testCVTStringToFormatDateTimeExpectTime("9", "FF1", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9000), cb); + testCVTStringToFormatDateTimeExpectTime("0", "SSSSS", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("00000", "SSSSS", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTime("45315", "SSSSS", createTimeStampTZ(0, 0, 0, 12, 35, 15, 0), cb); + testCVTStringToFormatDateTimeExpectTime("86399", "SSSSS", createTimeStampTZ(0, 0, 0, 23, 59, 59, 0), cb); - testCVTStringToFormatDateTimeExpectTime("1", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 100), cb); - testCVTStringToFormatDateTimeExpectTime("10", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); - testCVTStringToFormatDateTimeExpectTime("50", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); - testCVTStringToFormatDateTimeExpectTime("99", "FF2", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9900), cb); + testCVTStringToFormatDateTimeExpectTime("1", "FF1", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTimeExpectTime("5", "FF1", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTimeExpectTime("9", "FF1", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 9000), cb); - testCVTStringToFormatDateTimeExpectTime("1", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 10), cb); - testCVTStringToFormatDateTimeExpectTime("10", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 100), cb); - testCVTStringToFormatDateTimeExpectTime("100", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); - testCVTStringToFormatDateTimeExpectTime("500", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); - testCVTStringToFormatDateTimeExpectTime("999", "FF3", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9990), cb); + testCVTStringToFormatDateTimeExpectTime("1", "FF2", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 100), cb); + testCVTStringToFormatDateTimeExpectTime("10", "FF2", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTimeExpectTime("50", "FF2", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTimeExpectTime("99", "FF2", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 9900), cb); - testCVTStringToFormatDateTimeExpectTime("1", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1), cb); - testCVTStringToFormatDateTimeExpectTime("10", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 10), cb); - testCVTStringToFormatDateTimeExpectTime("100", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 100), cb); - testCVTStringToFormatDateTimeExpectTime("1000", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 1000), cb); - testCVTStringToFormatDateTimeExpectTime("5000", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 5000), cb); - testCVTStringToFormatDateTimeExpectTime("9999", "FF4", createTimeStampTZ(1, 1, 1, 0, 0, 0, 0, 9999), cb); + testCVTStringToFormatDateTimeExpectTime("1", "FF3", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 10), cb); + testCVTStringToFormatDateTimeExpectTime("10", "FF3", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 100), cb); + testCVTStringToFormatDateTimeExpectTime("100", "FF3", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTimeExpectTime("500", "FF3", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTimeExpectTime("999", "FF3", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 9990), cb); - testCVTStringToFormatDateTimeExpectTime("1 PM - 25 - 45 - 200", "HH.MI.SS.FF4", createTimeStampTZ(1, 1, 1, 13, 25, 45, 0, 200), cb); - testCVTStringToFormatDateTimeExpectTime("15:0:15:2", "HH24.MI.SS.FF1", createTimeStampTZ(1, 1, 1, 15, 0, 15, 0, 2000), cb); + testCVTStringToFormatDateTimeExpectTime("1", "FF4", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 1), cb); + testCVTStringToFormatDateTimeExpectTime("10", "FF4", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 10), cb); + testCVTStringToFormatDateTimeExpectTime("100", "FF4", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 100), cb); + testCVTStringToFormatDateTimeExpectTime("1000", "FF4", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 1000), cb); + testCVTStringToFormatDateTimeExpectTime("5000", "FF4", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 5000), cb); + testCVTStringToFormatDateTimeExpectTime("9999", "FF4", createTimeStampTZ(0, 0, 0, 0, 0, 0, 0, 9999), cb); + + testCVTStringToFormatDateTimeExpectTime("1 P.M. - 25 - 45 - 200", "HH P.M. MI.SS.FF4", createTimeStampTZ(0, 0, 0, 13, 25, 45, 0, 200), cb); + testCVTStringToFormatDateTimeExpectTime("15:0:15:2", "HH24.MI.SS.FF1", createTimeStampTZ(0, 0, 0, 15, 0, 15, 0, 2000), cb); } BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_TZ) { - testCVTStringToFormatDateTimeExpectTimeTZ("12:00 2:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 150, 0), cb); - testCVTStringToFormatDateTimeExpectTimeTZ("12:00 +2:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 150, 0), cb); - testCVTStringToFormatDateTimeExpectTimeTZ("12:00 -2:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, -150, 0), cb); - testCVTStringToFormatDateTimeExpectTimeTZ("12:00 +0:30", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 30, 0), cb); - testCVTStringToFormatDateTimeExpectTimeTZ("12:00 +0:00", "HH24:MI TZH:TZM", createTimeStampTZ(1, 1, 1, 12, 0, 0, 0, 0), cb); + testCVTStringToFormatDateTimeExpectTimeTZ("12:00 2:30", "HH24:MI TZH:TZM", createTimeStampTZ(0, 0, 0, 12, 0, 0, 150, 0), cb); + testCVTStringToFormatDateTimeExpectTimeTZ("12:00 +2:30", "HH24:MI TZH:TZM", createTimeStampTZ(0, 0, 0, 12, 0, 0, 150, 0), cb); + testCVTStringToFormatDateTimeExpectTimeTZ("12:00 -2:30", "HH24:MI TZH:TZM", createTimeStampTZ(0, 0, 0, 12, 0, 0, -150, 0), cb); + testCVTStringToFormatDateTimeExpectTimeTZ("12:00 +0:30", "HH24:MI TZH:TZM", createTimeStampTZ(0, 0, 0, 12, 0, 0, 30, 0), cb); + testCVTStringToFormatDateTimeExpectTimeTZ("12:00 +0:00", "HH24:MI TZH:TZM", createTimeStampTZ(0, 0, 0, 12, 0, 0, 0, 0), cb); } BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_SOLID_PATTERNS) { - testCVTStringToFormatDateTimeExpectTime("1 PM - 25 - 45 - 200", "HHMISSFF4", createTimeStampTZ(1, 1, 1, 13, 25, 45, 0, 200), cb); + testCVTStringToFormatDateTimeExpectTime("1 P.M. - 25 - 45 - 200", "HHA.M.MISSFF4", createTimeStampTZ(0, 0, 0, 13, 25, 45, 0, 200), cb); testCVTStringToFormatDateTimeExpectDate("1981-8/13", "YEARMMDD", createTimeStampTZ(1981, 8, 13, 0, 0, 0, 0), cb); } +BOOST_AUTO_TEST_CASE(CVTStringToFormatDateTime_EXCEPTION_CHECK) +{ + testExceptionCvtStringToFormatDateTime("2000.12.11", "WRONG FORMAT", cb); + testExceptionCvtStringToFormatDateTime("2000.12.11", "YYYY.MM.DD SS", cb); + testExceptionCvtStringToFormatDateTime("2000.12", "YYYY.MM.DD", cb); + + testExceptionCvtStringToFormatDateTime("1 A.G.", "HH12 A.M.", cb); + testExceptionCvtStringToFormatDateTime("1 A.G.", "HH12 A.P.", cb); + + testExceptionCvtStringToFormatDateTime("24 12 A.M.", "HH24 HH12 P.M.", cb); + testExceptionCvtStringToFormatDateTime("24 12", "HH24 HH12", cb); + testExceptionCvtStringToFormatDateTime("24 A.M.", "HH24 P.M.", cb); + testExceptionCvtStringToFormatDateTime("12", "HH12", cb); + testExceptionCvtStringToFormatDateTime("A.M.", "P.M.", cb); + testExceptionCvtStringToFormatDateTime("P.M.", "A.M.", cb); + + testExceptionCvtStringToFormatDateTime("1 1 A.M.", "SSSSS HH12 A.M.", cb); + testExceptionCvtStringToFormatDateTime("1 1 A.M.", "SSSSS HH12 P.M.", cb); + testExceptionCvtStringToFormatDateTime("1 1 A.M.", "SSSSS HH A.M.", cb); + testExceptionCvtStringToFormatDateTime("1 1 A.M.", "SSSSS HH P.M.", cb); + testExceptionCvtStringToFormatDateTime("1 1", "SSSSS HH24", cb); + testExceptionCvtStringToFormatDateTime("1 1", "SSSSS MI", cb); + testExceptionCvtStringToFormatDateTime("1 1", "SSSSS SS", cb); + + testExceptionCvtStringToFormatDateTime("30 1", "TZM SS", cb); + testExceptionCvtStringToFormatDateTime("30", "TZM", cb); +} + BOOST_AUTO_TEST_SUITE_END() // FunctionalTest BOOST_AUTO_TEST_SUITE_END() // CVTStringToFormatDateTime diff --git a/src/common/tests/CvtTestUtils.h b/src/common/tests/CvtTestUtils.h index beafaf7055..fc0ea1eb90 100644 --- a/src/common/tests/CvtTestUtils.h +++ b/src/common/tests/CvtTestUtils.h @@ -26,22 +26,38 @@ namespace CvtTestUtils { template static constexpr int sign(T value) { - return (T(0) < value) - (value < T(0)); + return (value >= T(0)) ? 1 : -1; } +static ISC_DATE mockGetLocalDate(int year = 2023) +{ + struct tm time; + memset(&time, 0, sizeof(time)); + time.tm_year = year - 1900; + time.tm_mon = 0; + time.tm_mday = 1; + return NoThrowTimeStamp::encode_date(&time); +} + + +// Pass 0 to year, month and day to use CurrentTimeStamp for them static struct tm initTMStruct(int year, int month, int day) { + struct tm currentTime; + NoThrowTimeStamp::decode_date(mockGetLocalDate(), ¤tTime); + struct tm times; memset(×, 0, sizeof(struct tm)); - times.tm_year = year - 1900; - times.tm_mon = month - 1; - times.tm_mday = day; + times.tm_year = year > 0 ? year - 1900 : currentTime.tm_year; + times.tm_mon = month > 0 ? month - 1 : currentTime.tm_mon; + times.tm_mday = day > 0 ? day : currentTime.tm_mday; mktime(×); return times; } +// Pass 0 to year, month and day to use CurrentTimeStamp for them static ISC_DATE createDate(int year, int month, int day) { struct tm times = initTMStruct(year, month, day); @@ -53,6 +69,7 @@ static ISC_TIME createTime(int hours, int minutes, int seconds, int fractions = return NoThrowTimeStamp::encode_time(hours, minutes, seconds, fractions); } +// Pass 0 to year, month and day to use CurrentTimeStamp for them 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); @@ -63,6 +80,7 @@ static ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, in return NoThrowTimeStamp::encode_timestamp(×, fractions); } +// Pass 0 to year, month and day to use CurrentTimeStamp for them static ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0) { @@ -84,10 +102,11 @@ static ISC_TIME_TZ createTimeTZ(int hours, int minutes, int seconds, int offsetI } -class CVTCallback : public Firebird::Callbacks +class MockCallback : public Firebird::Callbacks { public: - explicit CVTCallback(ErrorFunction aErr) : Callbacks(aErr) + explicit MockCallback(ErrorFunction aErr, std::function mockGetLocalDateFunc) + : Callbacks(aErr), m_mockGetLocalDateFunc(mockGetLocalDateFunc) {} public: @@ -97,10 +116,18 @@ public: void validateData(Firebird::CharSet* toCharset, SLONG length, const UCHAR* q) override { } ULONG validateLength(Firebird::CharSet* charSet, CHARSET_ID charSetId, ULONG length, const UCHAR* start, const USHORT size) override { return 0; } - SLONG getLocalDate() override { return 0; } + + SLONG getLocalDate() override + { + return m_mockGetLocalDateFunc(); + } + ISC_TIMESTAMP getCurrentGmtTimeStamp() override { ISC_TIMESTAMP ts; return ts; } USHORT getSessionTimeZone() override { return 1439; } // 1439 is ONE_DAY, so we have no offset void isVersion4(bool& v4) override { } + +private: + std::function m_mockGetLocalDateFunc; }; diff --git a/src/dsql/ExprNodes.cpp b/src/dsql/ExprNodes.cpp index 2933e87eb0..67a200fd27 100644 --- a/src/dsql/ExprNodes.cpp +++ b/src/dsql/ExprNodes.cpp @@ -3815,38 +3815,38 @@ dsc* CastNode::perform(thread_db* tdbb, impure_value* impure, dsc* value, { case dtype_sql_time: { - ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, &EngineCallbacks::instance, - expect_sql_time); + ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, expect_sql_time, + &EngineCallbacks::instance); *(ISC_TIME*) impure->vlu_desc.dsc_address = timestampTZ.utc_timestamp.timestamp_time; break; } case dtype_sql_date: { - ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, &EngineCallbacks::instance, - expect_sql_date); + ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, expect_sql_date, + &EngineCallbacks::instance); *(ISC_DATE*) impure->vlu_desc.dsc_address = timestampTZ.utc_timestamp.timestamp_date; break; } case dtype_timestamp: { - ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, &EngineCallbacks::instance, - expect_timestamp); + ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, expect_timestamp, + &EngineCallbacks::instance); *(ISC_TIMESTAMP*) impure->vlu_desc.dsc_address = timestampTZ.utc_timestamp; break; } case dtype_sql_time_tz: case dtype_ex_time_tz: { - ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, &EngineCallbacks::instance, - expect_sql_time_tz); + ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, expect_sql_time_tz, + &EngineCallbacks::instance); *(ISC_TIME_TZ*) impure->vlu_desc.dsc_address = TimeZoneUtil::timeStampTzToTimeTz(timestampTZ); break; } case dtype_timestamp_tz: case dtype_ex_timestamp_tz: { - ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, &EngineCallbacks::instance, - expect_timestamp_tz); + ISC_TIMESTAMP_TZ timestampTZ = CVT_string_to_format_datetime(value, format, expect_timestamp_tz, + &EngineCallbacks::instance); *(ISC_TIMESTAMP_TZ*) impure->vlu_desc.dsc_address = timestampTZ; break; } diff --git a/src/include/firebird/impl/msg/jrd.h b/src/include/firebird/impl/msg/jrd.h index 0c12476bf8..346adcfb64 100644 --- a/src/include/firebird/impl/msg/jrd.h +++ b/src/include/firebird/impl/msg/jrd.h @@ -978,6 +978,9 @@ FB_IMPL_MSG(JRD, 975, invalid_data_type_for_date_format, -901, "HY", "000", "It 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, 979, incorrect_hours_period, -901, "HY", "000", "@1 is incorrect period for 12H, it should be A.M. or P.M.") 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\"") +FB_IMPL_MSG(JRD, 982, pattern_cant_be_used_without_other_pattern, -901, "HY", "000", "@1 can't be used without @2") +FB_IMPL_MSG(JRD, 983, pattern_cant_be_used_without_other_pattern_and_vice_versa, -901, "HY", "000", "@1 can't be used without @2 and vice versa") +FB_IMPL_MSG(JRD, 984, incompatible_format_patterns, -901, "HY", "000", "@1 incompatible with @2") diff --git a/src/include/gen/Firebird.pas b/src/include/gen/Firebird.pas index bfde9bc087..2644e03fbb 100644 --- a/src/include/gen/Firebird.pas +++ b/src/include/gen/Firebird.pas @@ -5724,6 +5724,9 @@ const isc_incorrect_hours_period = 335545299; isc_data_for_format_is_exhausted = 335545300; isc_trailing_part_of_string = 335545301; + isc_pattern_cant_be_used_without_other_pattern = 335545302; + isc_pattern_cant_be_used_without_other_pattern_and_vice_versa = 335545303; + isc_incompatible_format_patterns = 335545304; isc_gfix_db_name = 335740929; isc_gfix_invalid_sw = 335740930; isc_gfix_incmp_sw = 335740932; diff --git a/src/jrd/blp.h b/src/jrd/blp.h index 23f2a31d76..085cb30e78 100644 --- a/src/jrd/blp.h +++ b/src/jrd/blp.h @@ -251,13 +251,15 @@ static const struct // New BLR in FB5.0 {"dcl_local_table", dcl_local_table}, {"local_table_truncate", one_word}, - {"local_table_id", local_table}, + {"local_table_id", local_table}, // 220 {"outer_map", outer_map}, {NULL, NULL}, // blr_json_function {"skip_locked", zero}, + // New BLR in FB6.0 {"invoke_function", invoke_function}, {"invoke_procedure", invsel_procedure}, {"select_procedure", invsel_procedure}, {"blr_default_arg", zero}, + {"cast_format", cast_format}, {0, 0} }; diff --git a/src/yvalve/gds.cpp b/src/yvalve/gds.cpp index 57f2b0f726..c19ee28f53 100644 --- a/src/yvalve/gds.cpp +++ b/src/yvalve/gds.cpp @@ -419,7 +419,8 @@ static const UCHAR outer_map[] = { op_outer_map, 0 }, in_list[] = { op_line, op_verb, op_indent, op_word, op_line, op_args, 0}, invoke_function[] = { op_invoke_function, 0 }, - invsel_procedure[] = { op_invsel_procedure, 0 }; + invsel_procedure[] = { op_invsel_procedure, 0 }, + cast_format[] = { op_byte, op_literal, op_dtype, op_line, op_verb, 0 }; #include "../jrd/blp.h"