Enhancements to date/time functions:

(1) Add support for time-shift modifiers of the form (+|-)YYYY-MM-DD HH:MM:SS.SSS
(2) Add the timediff(A,B) SQL function that returns the time-shift modifier
sufficient to move B to A.

FossilOrigin-Name: 574bf370b0b9a72eb0468e0e121d4d6171fc533883b66d335f6794ef5ee1f051
This commit is contained in:
drh 2023-05-30 16:40:42 +00:00
commit 8eb9c06e65
4 changed files with 340 additions and 25 deletions

View File

@ -1,5 +1,5 @@
C Minor\stweak\sto\sthe\sOPFS\sasync\sproxy\sto\sattempt\sto\saccount\sfor\sa\sspurious\shigh-load\sconsole\serror\smessage\swhich\swas\sreported\svia\sthe\ssqlite-wasm\snpm\sproject. C Enhancements\sto\sdate/time\sfunctions:\n(1)\sAdd\ssupport\sfor\stime-shift\smodifiers\sof\sthe\sform\s(+|-)YYYY-MM-DD\sHH:MM:SS.SSS\n(2)\sAdd\sthe\stimediff(A,B)\sSQL\sfunction\sthat\sreturns\sthe\stime-shift\smodifier\nsufficient\sto\smove\sB\sto\sA.
D 2023-05-30T14:10:28.641 D 2023-05-30T16:40:42.849
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@ -583,7 +583,7 @@ F src/build.c 5512d5a335334b48d116f1ecd051edef96a60add18ae48e0ea302a395f00f3d9
F src/callback.c db3a45e376deff6a16c0058163fe0ae2b73a2945f3f408ca32cf74960b28d490 F src/callback.c db3a45e376deff6a16c0058163fe0ae2b73a2945f3f408ca32cf74960b28d490
F src/complete.c a3634ab1e687055cd002e11b8f43eb75c17da23e F src/complete.c a3634ab1e687055cd002e11b8f43eb75c17da23e
F src/ctime.c 20507cc0b0a6c19cd882fcd0eaeda32ae6a4229fb4b024cfdf3183043d9b703d F src/ctime.c 20507cc0b0a6c19cd882fcd0eaeda32ae6a4229fb4b024cfdf3183043d9b703d
F src/date.c aca9e0c08b400b21238b609aea7c09585396cd770985cf8f475560f69222dad3 F src/date.c cb71f73d5df5303ec6630971b9c8b8c7df466fa3233dbdc643d613806e53e554
F src/dbpage.c f3eea5f7ec47e09ee7da40f42b25092ecbe961fc59566b8e5f705f34335b2387 F src/dbpage.c f3eea5f7ec47e09ee7da40f42b25092ecbe961fc59566b8e5f705f34335b2387
F src/dbstat.c ec92074baa61d883de58c945162d9e666c13cd7cf3a23bc38b4d1c4d0b2c2bef F src/dbstat.c ec92074baa61d883de58c945162d9e666c13cd7cf3a23bc38b4d1c4d0b2c2bef
F src/delete.c 05e27e3a55dcfeadf2f7ca95a5c5e0928f182c04640ec1954ffa42f3d5c19341 F src/delete.c 05e27e3a55dcfeadf2f7ca95a5c5e0928f182c04640ec1954ffa42f3d5c19341
@ -1605,6 +1605,7 @@ F test/threadtest3.c 655bff6c0895ec03f014126aa65e808fac9aae8c5a7a7da58a510cbe8b4
F test/threadtest4.c c1e67136ceb6c7ec8184e56ac61db28f96bd2925 F test/threadtest4.c c1e67136ceb6c7ec8184e56ac61db28f96bd2925
F test/threadtest5.c 9b4d782c58d8915d7e955ff8051f3d03628bda0d33b82971ea8c0f2f2808c421 F test/threadtest5.c 9b4d782c58d8915d7e955ff8051f3d03628bda0d33b82971ea8c0f2f2808c421
F test/time-wordcount.sh 8e0b0f8109367827ad5d58f5cc849705731e4b90 F test/time-wordcount.sh 8e0b0f8109367827ad5d58f5cc849705731e4b90
F test/timediff1.test 635b0e1857d1a3519382ae1a0aff9580c33e550c5a5cbec600b5edd2e0487018
F test/tkt-02a8e81d44.test 6c80d9c7514e2a42d4918bf87bf6bc54f379110c F test/tkt-02a8e81d44.test 6c80d9c7514e2a42d4918bf87bf6bc54f379110c
F test/tkt-18458b1a.test 6a62cb1ee50fa3c620da59e3a6f531eb38fceaf7e2166203816b724524e6f1d6 F test/tkt-18458b1a.test 6a62cb1ee50fa3c620da59e3a6f531eb38fceaf7e2166203816b724524e6f1d6
F test/tkt-26ff0c2d1e.test c15bec890c4d226c0da2f35ff30f9e84c169cfef90e73a8cb5cec11d723dfa96 F test/tkt-26ff0c2d1e.test c15bec890c4d226c0da2f35ff30f9e84c169cfef90e73a8cb5cec11d723dfa96
@ -2071,8 +2072,9 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
P 68abee3c1bf4e9ff632fccaa6ac7c5c4cdf55c4ff1749830ae3bd5ff6d55ebb2 P a61467d1fbb7a2804bfdc685a38e0b738747b9afd42c25a78a938b114a822f61 d6954259bd3f8475f3804fd02a1591d037efa9df54da45776863c024ff90e596
R e35de080a5d8c160cf20199fde831e23 R 255227b4547145c8dcfb80a734de5898
U stephan T +closed d6954259bd3f8475f3804fd02a1591d037efa9df54da45776863c024ff90e596
Z a6e965e2f0231d43e4b927decea2e440 U drh
Z b0a2fbca407b458dcbae70cd95c3b03c
# Remove this line to create a well-formed Fossil manifest. # Remove this line to create a well-formed Fossil manifest.

View File

@ -1 +1 @@
a61467d1fbb7a2804bfdc685a38e0b738747b9afd42c25a78a938b114a822f61 574bf370b0b9a72eb0468e0e121d4d6171fc533883b66d335f6794ef5ee1f051

View File

@ -641,6 +641,25 @@ static const struct {
{ 4, "year", 14713.0, 31536000.0 }, { 4, "year", 14713.0, 31536000.0 },
}; };
/*
** If the DateTime p is raw number, try to figure out if it is
** a julian day number of a unix timestamp. Set the p value
** appropriately.
*/
static void autoAdjustDate(DateTime *p){
if( !p->rawS || p->validJD ){
p->rawS = 0;
}else if( p->s>=-21086676*(i64)10000 /* -4713-11-24 12:00:00 */
&& p->s<=(25340230*(i64)10000)+799 /* 9999-12-31 23:59:59 */
){
double r = p->s*1000.0 + 210866760000000.0;
clearYMD_HMS_TZ(p);
p->iJD = (sqlite3_int64)(r + 0.5);
p->validJD = 1;
p->rawS = 0;
}
}
/* /*
** Process a modifier to a date-time stamp. The modifiers are ** Process a modifier to a date-time stamp. The modifiers are
** as follows: ** as follows:
@ -684,19 +703,8 @@ static int parseModifier(
*/ */
if( sqlite3_stricmp(z, "auto")==0 ){ if( sqlite3_stricmp(z, "auto")==0 ){
if( idx>1 ) return 1; /* IMP: R-33611-57934 */ if( idx>1 ) return 1; /* IMP: R-33611-57934 */
if( !p->rawS || p->validJD ){ autoAdjustDate(p);
rc = 0; rc = 0;
p->rawS = 0;
}else if( p->s>=-21086676*(i64)10000 /* -4713-11-24 12:00:00 */
&& p->s<=(25340230*(i64)10000)+799 /* 9999-12-31 23:59:59 */
){
r = p->s*1000.0 + 210866760000000.0;
clearYMD_HMS_TZ(p);
p->iJD = (sqlite3_int64)(r + 0.5);
p->validJD = 1;
p->rawS = 0;
rc = 0;
}
} }
break; break;
} }
@ -862,18 +870,64 @@ static int parseModifier(
case '9': { case '9': {
double rRounder; double rRounder;
int i; int i;
for(n=1; z[n] && z[n]!=':' && !sqlite3Isspace(z[n]); n++){} int Y,M,D,h,m,x;
const char *z2 = z;
for(n=1; z[n]; n++){
if( z[n]==':' ) break;
if( sqlite3Isspace(z[n]) ) break;
if( z[n]=='-' && n==5 && getDigits(&z[1], "40f", &Y)==1 ) break;
}
if( sqlite3AtoF(z, &r, n, SQLITE_UTF8)<=0 ){ if( sqlite3AtoF(z, &r, n, SQLITE_UTF8)<=0 ){
rc = 1; rc = 1;
break; break;
} }
if( z[n]==':' ){ if( z[n]=='-' ){
/* A modifier of the form (+|-)YYYY-MM-DD adds or subtracts the
** specified number of years, months, and days. MM is limited to
** the range 0-11 and DD is limited to 0-30.
*/
if( z[0]!='+' && z[0]!='-' ) break; /* Must start with +/- */
if( NEVER(n!=5) ) break; /* Must be 4-digit YYYY */
if( getDigits(&z[1], "40f-20a-20d", &Y, &M, &D)!=3 ) break;
if( M>=12 ) break; /* M range 0..11 */
if( D>=31 ) break; /* D range 0..30 */
computeYMD_HMS(p);
p->validJD = 0;
if( z[0]=='-' ){
p->Y -= Y;
p->M -= M;
D = -D;
}else{
p->Y += Y;
p->M += M;
}
x = p->M>0 ? (p->M-1)/12 : (p->M-12)/12;
p->Y += x;
p->M -= x*12;
computeJD(p);
p->validHMS = 0;
p->validYMD = 0;
p->iJD += (i64)D*86400000;
if( z[11]==0 ){
rc = 0;
break;
}
if( sqlite3Isspace(z[11])
&& getDigits(&z[12], "20c:20e", &h, &m)==2
){
z2 = &z[12];
n = 2;
}else{
break;
}
}
if( z2[n]==':' ){
/* A modifier of the form (+|-)HH:MM:SS.FFF adds (or subtracts) the /* A modifier of the form (+|-)HH:MM:SS.FFF adds (or subtracts) the
** specified number of hours, minutes, seconds, and fractional seconds ** specified number of hours, minutes, seconds, and fractional seconds
** to the time. The ".FFF" may be omitted. The ":SS.FFF" may be ** to the time. The ".FFF" may be omitted. The ":SS.FFF" may be
** omitted. ** omitted.
*/ */
const char *z2 = z;
DateTime tx; DateTime tx;
sqlite3_int64 day; sqlite3_int64 day;
if( !sqlite3Isdigit(*z2) ) z2++; if( !sqlite3Isdigit(*z2) ) z2++;
@ -908,7 +962,6 @@ static int parseModifier(
){ ){
switch( i ){ switch( i ){
case 4: { /* Special processing to add months */ case 4: { /* Special processing to add months */
int x;
assert( strcmp(aXformType[i].zName,"month")==0 ); assert( strcmp(aXformType[i].zName,"month")==0 );
computeYMD_HMS(p); computeYMD_HMS(p);
p->M += (int)r; p->M += (int)r;
@ -1325,6 +1378,116 @@ static void cdateFunc(
dateFunc(context, 0, 0); dateFunc(context, 0, 0);
} }
/*
** timediff(DATE1, DATE2)
**
** Return the amount of time that must be added to DATE2 in order to
** convert it into DATE2. The time difference format is:
**
** +YYYY-MM-DD HH:MM:SS.SSS
**
** The initial "+" becomes "-" if DATE1 occurs before DATE2. For
** date/time values A and B, the following invariant should hold:
**
** datetime(A) == (datetime(B, timediff(A,B))
**
** Both DATE arguments must be either a julian day number, or an
** ISO-8601 string. The unix timestamps are not supported by this
** routine.
*/
static void timediffFunc(
sqlite3_context *context,
int argc,
sqlite3_value **argv
){
char sign;
int Y, M;
DateTime d1, d2;
sqlite3_str sRes;
if( isDate(context, 1, argv, &d1) ) return;
if( isDate(context, 1, &argv[1], &d2) ) return;
computeYMD_HMS(&d1);
computeYMD_HMS(&d2);
if( d1.iJD>=d2.iJD ){
sign = '+';
Y = d1.Y - d2.Y;
if( Y ){
d2.Y = d1.Y;
d2.validJD = 0;
computeJD(&d2);
}
M = d1.M - d2.M;
if( M<0 ){
Y--;
M += 12;
}
if( M!=0 ){
d2.M = d1.M;
d2.validJD = 0;
computeJD(&d2);
}
if( d1.iJD<d2.iJD ){
M--;
if( M<0 ){
M = 11;
Y--;
}
d2.M--;
if( d2.M<1 ){
d2.M = 12;
d2.Y--;
}
d2.validJD = 0;
computeJD(&d2);
}
d1.iJD -= d2.iJD;
d1.iJD += 148699540800000;
}else{
sign = '-';
Y = d2.Y - d1.Y;
if( Y ){
d2.Y = d1.Y;
d2.validJD = 0;
computeJD(&d2);
}
M = d2.M - d1.M;
if( M<0 ){
Y--;
M += 12;
}
if( M!=0 ){
d2.M = d1.M;
d2.validJD = 0;
computeJD(&d2);
}
if( d1.iJD>d2.iJD ){
M--;
if( M<0 ){
M = 11;
Y--;
}
d2.M++;
if( d2.M>12 ){
d2.M = 1;
d2.Y++;
}
d2.validJD = 0;
computeJD(&d2);
}
d1.iJD = d2.iJD - d1.iJD;
d1.iJD += 148699540800000;
}
d1.validYMD = 0;
d1.validHMS = 0;
d1.validTZ = 0;
computeYMD_HMS(&d1);
sqlite3StrAccumInit(&sRes, 0, 0, 0, 100);
sqlite3_str_appendf(&sRes, "%c%04d-%02d-%02d %02d:%02d:%06.3f",
sign, Y, M, d1.D-1, d1.h, d1.m, d1.s);
sqlite3ResultStrAccum(context, &sRes);
}
/* /*
** current_timestamp() ** current_timestamp()
** **
@ -1399,6 +1562,7 @@ void sqlite3RegisterDateTimeFunctions(void){
PURE_DATE(time, -1, 0, 0, timeFunc ), PURE_DATE(time, -1, 0, 0, timeFunc ),
PURE_DATE(datetime, -1, 0, 0, datetimeFunc ), PURE_DATE(datetime, -1, 0, 0, datetimeFunc ),
PURE_DATE(strftime, -1, 0, 0, strftimeFunc ), PURE_DATE(strftime, -1, 0, 0, strftimeFunc ),
PURE_DATE(timediff, 2, 0, 0, timediffFunc ),
DFUNCTION(current_time, 0, 0, 0, ctimeFunc ), DFUNCTION(current_time, 0, 0, 0, ctimeFunc ),
DFUNCTION(current_timestamp, 0, 0, 0, ctimestampFunc), DFUNCTION(current_timestamp, 0, 0, 0, ctimestampFunc),
DFUNCTION(current_date, 0, 0, 0, cdateFunc ), DFUNCTION(current_date, 0, 0, 0, cdateFunc ),

149
test/timediff1.test Normal file
View File

@ -0,0 +1,149 @@
# 2023-05-30
#
# The author disclaims copyright to this source code. In place of
# a legal notice, here is a blessing:
#
# May you do good and not evil.
# May you find forgiveness for yourself and forgive others.
# May you share freely, never taking more than you give.
#
#***********************************************************************
# This file implements regression tests for SQLite library. The
# focus of this file is testing date and time functions.
#
set testdir [file dirname $argv0]
source $testdir/tester.tcl
# Skip this whole file if date and time functions are omitted
# at compile-time
#
ifcapable {!datetime} {
finish_test
return
}
proc datetest {tnum expr result} {
do_test timediff-$tnum [subst {
execsql "SELECT coalesce($expr,'NULL')"
}] [list $result]
}
set tcl_precision 15
# February overflow on a leap year
datetest 1.1 {datetime('2000-01-31','+1 month')} {2000-03-02 00:00:00}
datetest 1.2 {datetime('2004-01-29','+1 month')} {2004-02-29 00:00:00}
datetest 1.3 {datetime('2000-03-01','-1 day')} {2000-02-29 00:00:00}
datetest 1.4 {datetime('2000-03-31','-1 month')} {2000-03-02 00:00:00}
datetest 1.5 {datetime('2000-03-30','-1 month')} {2000-03-01 00:00:00}
datetest 1.6 {datetime('2000-03-29','-1 month')} {2000-02-29 00:00:00}
datetest 1.7 {datetime('2000-03-28','-1 month')} {2000-02-28 00:00:00}
datetest 1.8 {datetime('2000-02-29','+1 year')} {2001-03-01 00:00:00}
datetest 1.9 {datetime('2000-02-29','+4 years')} {2004-02-29 00:00:00}
datetest 1.10 {datetime('1998-11-10','+0001-03-19 12:34:56')} \
{2000-02-29 12:34:56}
datetest 1.11 {datetime('2000-01-31','+0004-01-00 12:34:56')} \
{2004-03-02 12:34:56}
datetest 1.12 {datetime('2000-01-29','+0008-01-00 12:34:56')} \
{2008-02-29 12:34:56}
datetest 1.13 {datetime('2001-03-31','-0001-01-00 06:10')} \
{2000-03-01 17:50:00}
# February overflow on a non-leap year
datetest 2.1 {datetime('2001-01-31','+1 month')} {2001-03-03 00:00:00}
datetest 2.2 {datetime('2005-01-29','+1 month')} {2005-03-01 00:00:00}
datetest 2.3 {datetime('2001-03-01','-1 day')} {2001-02-28 00:00:00}
datetest 2.4 {datetime('2001-03-31','-1 month')} {2001-03-03 00:00:00}
datetest 2.5 {datetime('2001-03-30','-1 month')} {2001-03-02 00:00:00}
datetest 2.6 {datetime('2001-03-29','-1 month')} {2001-03-01 00:00:00}
datetest 2.7 {datetime('2001-03-28','-1 month')} {2001-02-28 00:00:00}
datetest 2.10 {datetime('1999-11-10','+0001-03-19 12:34:56')} \
{2001-03-01 12:34:56}
datetest 2.11 {datetime('2000-01-31','+0005-01-00 12:34:56')} \
{2005-03-03 12:34:56}
datetest 2.12 {datetime('2000-01-29','+0009-01-00 12:34:56')} \
{2009-03-01 12:34:56}
datetest 2.13 {datetime('2002-03-31','-0001-01-00 06:10')} \
{2001-03-02 17:50:00}
# timediff
datetest 3.1 {timediff('2000-03-02','2000-01-31')} {+0000-01-00 00:00:00.000}
datetest 3.2 {timediff('2000-01-31','2000-03-02')} {-0000-01-02 00:00:00.000}
datetest 3.3 {timediff('2000-03-02','1999-01-31')} {+0001-01-00 00:00:00.000}
datetest 3.4 {timediff('1999-01-31','2000-03-02')} {-0001-01-02 00:00:00.000}
unset -nocomplain p1
unset -nocomplain p2
set p1 {
1 {0000-01-01 12:34:56}
2 {1776-07-04 13:00:00}
3 {1969-07-20 20:17}
4 {2440587.5}
5 {2000-05-29 14:26}
6 {2023-05-29 18:11}
7 {2050-05-29 14:26}
}
set p2 {
A {1066-10-14}
B {1900-02-28 11:00}
C {1900-03-01 12:00}
D {1904-02-29 11:25}
E {2000-02-29 13:00}
E {2000-03-01 14:00}
F {2001-03-31 15:15}
G {2002-04-01 16:59}
H {2003-04-30 17:00}
I {2004-05-01 23:59:59}
J {2005-06-01}
K {2006-06-30 01:23:45}
L {2007-12-31 02:00}
M {2008-01-01 01:59}
}
foreach {x1 d1} $p1 {
foreach {x2 d2} $p2 {
set r1 [db one {SELECT datetime($d1)}]
do_execsql_test timediff-4-$x1$x2 {
SELECT datetime($d2, timediff($d1,$d2));
} [list $r1]
set r2 [db one {SELECT datetime($d2)}]
do_execsql_test timediff-4-$x2$x1 {
SELECT datetime($d1, timediff($d2,$d1));
} [list $r2]
}
}
# Partial time-diffs as modifiers
#
datetest 5-1 {datetime('2000-01-01','+0001-02-03')} {2001-03-04 00:00:00}
datetest 5-2 {datetime('2000-01-01','+0001-02-03x')} {NULL}
datetest 5-3 {datetime('2000-01-01','+0001-11-03')} {2001-12-04 00:00:00}
datetest 5-4 {datetime('2000-01-01','+0001-12-03')} {NULL}
datetest 5-5 {datetime('2000-01-01','+0001-02-30')} {2001-03-31 00:00:00}
datetest 5-6 {datetime('2000-01-01','+0001-02-31')} {NULL}
datetest 5-7 {datetime('2000-01-01','+0001-02-03 0')} {NULL}
datetest 5-8 {datetime('2000-01-01','+0001-02-03 01')} {NULL}
datetest 5-9 {datetime('2000-01-01','+0001-02-03 01:')} {NULL}
datetest 5-10 {datetime('2000-01-01','+0001-02-03 01:0')} {NULL}
datetest 5-11 {datetime('2000-01-01','+0001-02-03 01:02')} {2001-03-04 01:02:00}
datetest 5-12 {datetime('2000-01-01','+0001-02-03 01:02:')} {NULL}
datetest 5-13 {datetime('2000-01-01','+0001-02-03 01:02:0')} {NULL}
datetest 5-14 {datetime('2000-01-01','+0001-02-03 01:02:03')} \
{2001-03-04 01:02:03}
datetest 5-15 {datetime('2000-01-01','+0001-02-03 01:02:03.')} NULL
datetest 5-16 {datetime('2000-01-01','+0001-02-03 01:02:03.5')} \
{2001-03-04 01:02:03}
datetest 5-17 {datetime('2000-01-01','+0001-02-03 01:02:03.50')} \
{2001-03-04 01:02:03}
datetest 5-18 {datetime('2000-01-01','+0001-02-03 01:02:03.500')} \
{2001-03-04 01:02:03}
datetest 5-19 {datetime('2000-01-01','+0001-02-03 01:02:03.500x')} {NULL}
datetest 5-20 {datetime('2000-01-01','+0001-02-03 01:02:03.500 x')} {NULL}
finish_test