Skip to content

Commit

Permalink
Add FORMAT clause to convert datetime types to string and vice versa #…
Browse files Browse the repository at this point in the history
…2388 (#7629)

* Add FORMAT clause to convert datetime types to string and vice versa

* Add tests for FORMAT clause

* Fixes after review

* Change TZD to TZR

* Change inline variables back to static

* Add README documentation

* Add ability to use " in raw string and ...

Use session timezone if timezone is not specified.
Add ability to use + sign in timezone offset.
Add truncating string exception.

* Move util methods from BOOST_AUTO_TEST_SUITE

* Switch back to inline variables

* Consider charset in the format string

* Add ability to write patterns without separators

* Use printf to add extra zeros

Also add extra zeros to the year patterns.

* Replace template exception with a plain function

* Clean code after review

* Fix bug with TZH:TZM when TZH is 0

* Add TZR to STRING to DATE

---------

Co-authored-by: Artyom Ivanov <artyom.ivanov@red-soft.ru>
  • Loading branch information
TreeHunter9 and Artyom Ivanov committed Oct 24, 2023
1 parent 1fe1abd commit 897ac0c
Show file tree
Hide file tree
Showing 17 changed files with 1,947 additions and 133 deletions.
95 changes: 95 additions & 0 deletions doc/README.cast.format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
## 1. DATETIME TO STRING

The following flags are currently implemented for datetime to string conversion:
| Format Pattern | Description |
| -------------- | ----------- |
| YEAR | Year (1 - 9999) |
| YYYY | Last 4 digits of Year (0001 - 9999) |
| YYY | Last 3 digits of Year (000 - 999) |
| YY | Last 2 digits of Year (00 - 99) |
| Y | Last 1 digits of Year (0 - 9) |
| Q | Quarter of the Year (1 - 4) |
| MM | Month (01 - 12) |
| MON | Short Month name (Apr) |
| MONTH | Full Month name (APRIL) |
| RM | Roman representation of the Month (I - XII) |
| WW | Week of the Year (01 - 53) |
| W | Week of the Month (1 - 5) |
| D | Day of the Week (1 - 7) |
| DAY | Full name of the Day (MONDAY) |
| DD | Day of the Month (01 - 31) |
| DDD | Day of the Year (001 - 366) |
| DY | Short name of the Day (Mon) |
| J | Julian Day (number of days since January 1, 4712 BC) |
| HH / HH12 | Hour of the Day (01 - 12) with period (AM, PM) |
| HH24 | Hour of the Day (00 - 23) |
| MI | Minutes (00 - 59) |
| SS | Seconds (00 - 59) |
| SSSSS | Seconds after midnight (0 - 86399) |
| FF1 - FF9 | Fractional seconds with the specified accuracy |
| TZH | Time zone in Hours (-14 - 14) |
| TZM | Time zone in Minutes (00 - 59) |
| TZR | Time zone Name |

The dividers are:
| Dividers |
| ------------- |
| . |
| / |
| , |
| ; |
| : |
| 'space' |
| - |

Patterns can be used without any dividers:
```
SELECT CAST(CURRENT_TIMESTAMP AS VARCHAR(50) FORMAT 'YEARMMDD HH24MISS') FROM RDB$DATABASE;
=========================
20230719 161757
```
However, be careful with patterns like `DDDDD`, it will be interpreted as `DDD` + `DD`.

It is possible to insert raw text into a format string with `""`: `... FORMAT '"Today is" DAY'` - Today is MONDAY. To add `"` in output raw string use `\"` (to print `\` use `\\`).
Also the format is case-insensitive, so `YYYY-MM` == `yyyy-mm`.
Example:
```
SELECT CAST(CURRENT_TIMESTAMP AS VARCHAR(45) FORMAT 'DD.MM.YEAR HH24:MI:SS "is" J "Julian day"') FROM RDB$DATABASE;
=========================
14.6.2023 15:41:29 is 2460110 Julian day
```

## 2. STRING TO DATETIME

The following flags are currently implemented for string to datetime conversion:
| Format Pattern | Description |
| ------------- | ------------- |
| YEAR | Year |
| YYYY | Last 4 digits of Year |
| YYY | Last 3 digits of Year |
| YY | Last 2 digits of Year |
| Y | Last 1 digits of Year |
| MM | Month (1 - 12) |
| MON | Short Month name (Apr) |
| MONTH | Full Month name (APRIL) |
| RM | Roman representation of the Month (I - XII) |
| DD | Day of the Month (1 - 31) |
| J | Julian Day (number of days since January 1, 4712 BC) |
| HH / HH12 | Hour of the Day (1 - 12) with period (AM, PM) |
| HH24 | Hour of the Day (0 - 23) |
| MI | Minutes (0 - 59) |
| SS | Seconds (0 - 59) |
| SSSSS | Seconds after midnight (0 - 86399) |
| FF1 - FF4 | Fractional seconds with the specified accuracy |
| TZH | Time zone in Hours (-14 - 14) |
| TZM | Time zone in Minutes (0 - 59) |
| TZR | Time zone Name |

Dividers are the same as for datetime to string conversion and can also be omitted.

Example:
```
SELECT CAST('2000.12.08 12:35:30.5000' AS TIMESTAMP FORMAT 'YEAR.MM.DD HH24:MI:SS.FF4') FROM RDB$DATABASE;
=====================
2000-12-08 12:35:30.5000
```
1 change: 1 addition & 0 deletions src/common/ParserTokens.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ PARSER_TOKEN(TOK_FLOOR, "FLOOR", true)
PARSER_TOKEN(TOK_FOLLOWING, "FOLLOWING", true)
PARSER_TOKEN(TOK_FOR, "FOR", false)
PARSER_TOKEN(TOK_FOREIGN, "FOREIGN", false)
PARSER_TOKEN(TOK_FORMAT, "FORMAT", true)
PARSER_TOKEN(TOK_FREE_IT, "FREE_IT", true)
PARSER_TOKEN(TOK_FROM, "FROM", false)
PARSER_TOKEN(TOK_FULL, "FULL", false)
Expand Down
28 changes: 14 additions & 14 deletions src/common/TimeZoneUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ namespace

static const TimeZoneDesc* getDesc(USHORT timeZone);
static inline bool isOffset(USHORT timeZone);
static USHORT makeFromOffset(int sign, unsigned tzh, unsigned tzm);
static inline SSHORT offsetZoneToDisplacement(USHORT timeZone);
static inline USHORT displacementToOffsetZone(SSHORT displacement);
static int parseNumber(const char*& p, const char* end);
Expand Down Expand Up @@ -634,6 +633,20 @@ void TimeZoneUtil::extractOffset(const ISC_TIME_TZ& timeTz, SSHORT* offset)
extractOffset(tsTz, offset);
}

// Makes a time zone id from offsets.
USHORT TimeZoneUtil::makeFromOffset(int sign, unsigned tzh, unsigned tzm)
{
if (!TimeZoneUtil::isValidOffset(sign, tzh, tzm))
{
string str;
str.printf("%s%02u:%02u", (sign == -1 ? "-" : "+"), tzh, tzm);
status_exception::raise(Arg::Gds(isc_invalid_timezone_offset) << str);
}

return (USHORT)displacementToOffsetZone((tzh * 60 + tzm) * sign);
}


// Converts a time from local to UTC.
void TimeZoneUtil::localTimeToUtc(ISC_TIME& time, ISC_USHORT timeZone)
{
Expand Down Expand Up @@ -1156,19 +1169,6 @@ static inline bool isOffset(USHORT timeZone)
return timeZone <= ONE_DAY * 2;
}

// Makes a time zone id from offsets.
static USHORT makeFromOffset(int sign, unsigned tzh, unsigned tzm)
{
if (!TimeZoneUtil::isValidOffset(sign, tzh, tzm))
{
string str;
str.printf("%s%02u:%02u", (sign == -1 ? "-" : "+"), tzh, tzm);
status_exception::raise(Arg::Gds(isc_invalid_timezone_offset) << str);
}

return (USHORT)displacementToOffsetZone((tzh * 60 + tzm) * sign);
}

// Gets the displacement from a offset-based time zone id.
static inline SSHORT offsetZoneToDisplacement(USHORT timeZone)
{
Expand Down
2 changes: 2 additions & 0 deletions src/common/TimeZoneUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ class TimeZoneUtil
static void extractOffset(const ISC_TIMESTAMP_TZ& timeStampTz, SSHORT* offset);
static void extractOffset(const ISC_TIME_TZ& timeTz, SSHORT* offset);

static USHORT makeFromOffset(int sign, unsigned tzh, unsigned tzm);

static void localTimeToUtc(ISC_TIME& time, ISC_USHORT timeZone);
static void localTimeToUtc(ISC_TIME_TZ& timeTz);

Expand Down
74 changes: 74 additions & 0 deletions src/common/classes/NoThrowTimeStamp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,80 @@ void NoThrowTimeStamp::round_time(ISC_TIME &ntime, const int precision)
ntime -= (ntime % period);
}

int NoThrowTimeStamp::convertGregorianDateToWeekDate(const struct tm& times)
{
// Algorithm for Converting Gregorian Dates to ISO 8601 Week Date by Rick McCarty, 1999
// http://personal.ecu.edu/mccartyr/ISOwdALG.txt

const int y = times.tm_year + 1900;
const int dayOfYearNumber = times.tm_yday + 1;

// Find the jan1Weekday for y (Monday=1, Sunday=7)
const int yy = (y - 1) % 100;
const int c = (y - 1) - yy;
const int g = yy + yy / 4;
const int jan1Weekday = 1 + (((((c / 100) % 4) * 5) + g) % 7);

// Find the weekday for y m d
const int h = dayOfYearNumber + (jan1Weekday - 1);
const int weekday = 1 + ((h - 1) % 7);

// Find if y m d falls in yearNumber y-1, weekNumber 52 or 53
int yearNumber, weekNumber;

if ((dayOfYearNumber <= (8 - jan1Weekday)) && (jan1Weekday > 4))
{
yearNumber = y - 1;
weekNumber = ((jan1Weekday == 5) || ((jan1Weekday == 6) &&
isLeapYear(yearNumber))) ? 53 : 52;
}
else
{
yearNumber = y;

// Find if y m d falls in yearNumber y+1, weekNumber 1
int i = isLeapYear(y) ? 366 : 365;

if ((i - dayOfYearNumber) < (4 - weekday))
{
yearNumber = y + 1;
weekNumber = 1;
}
}

// Find if y m d falls in yearNumber y, weekNumber 1 through 53
if (yearNumber == y)
{
int j = dayOfYearNumber + (7 - weekday) + (jan1Weekday - 1);
weekNumber = j / 7;
if (jan1Weekday > 4)
weekNumber--;
}

return weekNumber;
}

int NoThrowTimeStamp::convertGregorianDateToJulianDate(int year, int month, int day)
{
int jdn = (1461 * (year + 4800 + (month - 14)/12))/4 + (367 * (month - 2 - 12 * ((month - 14)/12)))
/ 12 - (3 * ((year + 4900 + (month - 14)/12)/100))/4 + day - 32075;
return jdn;
}

void NoThrowTimeStamp::convertJulianDateToGregorianDate(int jdn, int& outYear, int& outMonth, int& outDay)
{
int a = jdn + 32044;
int b = (4 * a +3 ) / 146097;
int c = a - (146097 * b) / 4;
int d = (4 * c + 3) / 1461;
int e = c - (1461 * d) / 4;
int m = (5 * e + 2) / 153;

outDay = e - (153 * m + 2) / 5 + 1;
outMonth = m + 3 - 12 * (m / 10);
outYear = 100 * b + d - 4800 + (m / 10);
}

// Encode timestamp from UNIX datetime structure
void NoThrowTimeStamp::encode(const struct tm* times, int fractions)
{
Expand Down
4 changes: 4 additions & 0 deletions src/common/classes/NoThrowTimeStamp.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ class NoThrowTimeStamp
static void add10msec(ISC_TIMESTAMP* v, SINT64 msec, SINT64 multiplier);
static void round_time(ISC_TIME& ntime, const int precision);

static int convertGregorianDateToWeekDate(const struct tm& times);
static int convertGregorianDateToJulianDate(int year, int month, int day);
static void convertJulianDateToGregorianDate(int jdn, int& outYear, int& outMonth, int& outDay);

static inline bool isLeapYear(const int year) noexcept
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
Expand Down
30 changes: 28 additions & 2 deletions src/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ const int HIGH_WORD = 0;
#endif
#endif

static const TEXT FB_SHORT_MONTHS[][4] =
inline const TEXT FB_SHORT_MONTHS[][4] =
{
"Jan", "Feb", "Mar",
"Apr", "May", "Jun",
Expand All @@ -980,7 +980,7 @@ static const TEXT FB_SHORT_MONTHS[][4] =
"\0"
};

static const TEXT* const FB_LONG_MONTHS_UPPER[] =
inline const TEXT* const FB_LONG_MONTHS_UPPER[] =
{
"JANUARY",
"FEBRUARY",
Expand All @@ -997,6 +997,32 @@ static const TEXT* const FB_LONG_MONTHS_UPPER[] =
0
};

// Starts with SUNDAY cuz tm.tm_wday starts with it
inline const TEXT FB_SHORT_DAYS[][4] =
{
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"\0"
};

// Starts with SUNDAY cuz tm.tm_wday starts with it
inline const TEXT* const FB_LONG_DAYS_UPPER[] =
{
"SUNDAY",
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"\0"
};

const FB_SIZE_T FB_MAX_SIZEOF = ~FB_SIZE_T(0); // Assume FB_SIZE_T is unsigned

inline FB_SIZE_T fb_strlen(const char* str)
Expand Down
Loading

0 comments on commit 897ac0c

Please sign in to comment.