From 1eb32293de0bc1f5a1ff4b2e1ef636affd396e32 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 10 Jan 2017 22:19:01 +0000 Subject: [temp-commit]: code review, DST-hole mistake by scan with relative time resolved; caching of UTC2Local / Local2UTC cherry picked --- generic/tclClock.c | 394 ++++++++++++++++++++++++++++------------------ generic/tclClockFmt.c | 220 +++++++++++++++++++++----- generic/tclDate.h | 84 +++++++++- library/clock.tcl | 13 +- tests-perf/clock.perf.tcl | 50 +++++- tests/clock.test | 87 ++++++++++ 6 files changed, 638 insertions(+), 210 deletions(-) diff --git a/generic/tclClock.c b/generic/tclClock.c index 2e7b854..7cc5d86 100644 --- a/generic/tclClock.c +++ b/generic/tclClock.c @@ -26,22 +26,6 @@ #endif /* - * Constants - */ - -#define JULIAN_DAY_POSIX_EPOCH 2440588 -#define GREGORIAN_CHANGE_DATE 2361222 -#define SECONDS_PER_DAY 86400 -#define JULIAN_SEC_POSIX_EPOCH (((Tcl_WideInt) JULIAN_DAY_POSIX_EPOCH) \ - * SECONDS_PER_DAY) -#define FOUR_CENTURIES 146097 /* days */ -#define JDAY_1_JAN_1_CE_JULIAN 1721424 -#define JDAY_1_JAN_1_CE_GREGORIAN 1721426 -#define ONE_CENTURY_GREGORIAN 36524 /* days */ -#define FOUR_YEARS 1461 /* days */ -#define ONE_YEAR 365 /* days */ - -/* * Table of the days in each month, leap and common years */ @@ -72,8 +56,6 @@ typedef enum ClockLiteral { LIT_MONTH, LIT_SECONDS, LIT_TZNAME, LIT_TZOFFSET, LIT_YEAR, - LIT_CURRENTYEARCENTURY, - LIT_YEAROFCENTURYSWITCH, LIT_TZDATA, LIT_GETSYSTEMTIMEZONE, LIT_SETUPTIMEZONE, @@ -96,8 +78,6 @@ static const char *const Literals[] = { "month", "seconds", "tzName", "tzOffset", "year", - "::tcl::clock::CurrentYearCentury", - "::tcl::clock::YearOfCenturySwitch", "::tcl::clock::TZData", "::tcl::clock::GetSystemTimeZone", "::tcl::clock::SetupTimeZone", @@ -106,41 +86,6 @@ static const char *const Literals[] = { #endif }; -#define CurrentYearCentury 2000 - -/* - * Structure containing the client data for [clock] - */ - -typedef struct ClockClientData { - size_t refCount; /* Number of live references. */ - Tcl_Obj **literals; /* Pool of object literals. */ - /* Cache for current clock parameters, imparted via "configure" */ - unsigned long LastTZEpoch; - int currentYearCentury; - int yearOfCenturySwitch; - Tcl_Obj *SystemTimeZone; - Tcl_Obj *SystemSetupTZData; - Tcl_Obj *GMTSetupTimeZone; - Tcl_Obj *GMTSetupTZData; - Tcl_Obj *AnySetupTimeZone; - Tcl_Obj *AnySetupTZData; - Tcl_Obj *LastUnnormSetupTimeZone; - Tcl_Obj *LastSetupTimeZone; - Tcl_Obj *LastSetupTZData; - /* Cache for last base (fast convert if base/tz not changed) */ - Tcl_Obj *lastBaseTimeZone; - TclDateFields lastBaseDate; - /* - /* [SB] TODO: back-port (from tclSE) the same date caching ... - * Cache for last date (fast convert if date parsed was the same) * / - struct { - DateInfo yy; - TclDateFields date; - } lastDate; - */ -} ClockClientData; - static const char *const eras[] = { "CE", "BCE", NULL }; /* @@ -161,13 +106,13 @@ TCL_DECLARE_MUTEX(clockMutex) * Function prototypes for local procedures in this file: */ -static int ConvertUTCToLocal(Tcl_Interp *, +static int ConvertUTCToLocal(ClientData clientData, Tcl_Interp *, TclDateFields *, Tcl_Obj *, int); static int ConvertUTCToLocalUsingTable(Tcl_Interp *, TclDateFields *, int, Tcl_Obj *const[]); static int ConvertUTCToLocalUsingC(Tcl_Interp *, TclDateFields *, int); -static int ConvertLocalToUTC(Tcl_Interp *, +static int ConvertLocalToUTC(ClientData clientData, Tcl_Interp *, TclDateFields *, Tcl_Obj *, int); static int ConvertLocalToUTCUsingTable(Tcl_Interp *, TclDateFields *, int, Tcl_Obj *const[]); @@ -191,9 +136,9 @@ static int ClockConvertlocaltoutcObjCmd( ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]); -static int ClockGetDateFields(Tcl_Interp *interp, - TclDateFields *fields, Tcl_Obj *tzdata, - int changeover); +static int ClockGetDateFields(ClientData clientData, + Tcl_Interp *interp, TclDateFields *fields, + Tcl_Obj *tzdata, int changeover); static int ClockGetdatefieldsObjCmd( ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]); @@ -330,8 +275,8 @@ TclClockInit( Tcl_IncrRefCount(data->literals[i]); } data->LastTZEpoch = 0; - data->currentYearCentury = -1; - data->yearOfCenturySwitch = -1; + data->currentYearCentury = ClockDefaultYearCentury; + data->yearOfCenturySwitch = ClockDefaultCenturySwitch; data->SystemTimeZone = NULL; data->SystemSetupTZData = NULL; data->GMTSetupTimeZone = NULL; @@ -342,7 +287,10 @@ TclClockInit( data->LastSetupTimeZone = NULL; data->LastSetupTZData = NULL; - data->lastBaseTimeZone = NULL; + data->lastBase.TimeZone = NULL; + data->UTC2Local.tzData = NULL; + data->UTC2Local.tzName = NULL; + data->Local2UTC.tzData = NULL; /* * Install the commands. @@ -377,8 +325,6 @@ ClockConfigureClear( ClockClientData *data) { data->LastTZEpoch = 0; - data->currentYearCentury = -1; - data->yearOfCenturySwitch = -1; Tcl_UnsetObjRef(data->SystemTimeZone); Tcl_UnsetObjRef(data->SystemSetupTZData); Tcl_UnsetObjRef(data->GMTSetupTimeZone); @@ -388,6 +334,11 @@ ClockConfigureClear( Tcl_UnsetObjRef(data->LastUnnormSetupTimeZone); Tcl_UnsetObjRef(data->LastSetupTimeZone); Tcl_UnsetObjRef(data->LastSetupTZData); + + Tcl_UnsetObjRef(data->lastBase.TimeZone); + Tcl_UnsetObjRef(data->UTC2Local.tzData); + Tcl_UnsetObjRef(data->UTC2Local.tzName); + Tcl_UnsetObjRef(data->Local2UTC.tzData); } static void @@ -397,7 +348,8 @@ ClockDeleteCmdProc( ClockClientData *data = clientData; int i; - if (data->refCount-- <= 1) { + data->refCount--; + if (data->refCount == 0) { for (i = 0; i < LIT__END; ++i) { Tcl_DecrRefCount(data->literals[i]); } @@ -471,10 +423,12 @@ ClockConfigureObjCmd( static const char *const options[] = { "-system-tz", "-setup-tz", "-clear", + "-year-century", "-century-switch", NULL }; enum optionInd { CLOCK_SYSTEM_TZ, CLOCK_SETUP_TZ, CLOCK_CLEAR_CACHE, + CLOCK_YEAR_CENTURY, CLOCK_CENTURY_SWITCH, CLOCK_SETUP_GMT, CLOCK_SETUP_NOP }; int optionIndex; /* Index of an option. */ @@ -542,6 +496,32 @@ ClockConfigureObjCmd( Tcl_SetObjResult(interp, dataPtr->LastSetupTimeZone); } break; + case CLOCK_YEAR_CENTURY: + if (i+1 < objc) { + int year; + if (TclGetIntFromObj(interp, objv[i+1], &year) != TCL_OK) { + return TCL_ERROR; + } + dataPtr->currentYearCentury = year; + Tcl_SetObjResult(interp, objv[i+1]); + continue; + } + Tcl_SetObjResult(interp, + Tcl_NewIntObj(dataPtr->currentYearCentury)); + break; + case CLOCK_CENTURY_SWITCH: + if (i+1 < objc) { + int year; + if (TclGetIntFromObj(interp, objv[i+1], &year) != TCL_OK) { + return TCL_ERROR; + } + dataPtr->yearOfCenturySwitch = year; + Tcl_SetObjResult(interp, objv[i+1]); + continue; + } + Tcl_SetObjResult(interp, + Tcl_NewIntObj(dataPtr->yearOfCenturySwitch)); + break; case CLOCK_CLEAR_CACHE: ClockConfigureClear(dataPtr); break; @@ -577,14 +557,16 @@ ClockGetTZData( /* simple caching, because almost used the tz-data of last timezone */ if (timezoneObj == dataPtr->SystemTimeZone) { - if (dataPtr->SystemSetupTZData != NULL) + if (dataPtr->SystemSetupTZData != NULL) { return dataPtr->SystemSetupTZData; + } out = &dataPtr->SystemSetupTZData; } else if (timezoneObj == dataPtr->GMTSetupTimeZone) { - if (dataPtr->GMTSetupTZData != NULL) + if (dataPtr->GMTSetupTZData != NULL) { return dataPtr->GMTSetupTZData; + } out = &dataPtr->GMTSetupTZData; } else @@ -602,11 +584,11 @@ ClockGetTZData( if (out != NULL) { Tcl_SetObjRef(*out, ret); } - Tcl_SetObjRef(dataPtr->LastSetupTZData, ret); + Tcl_SetObjRef(dataPtr->LastSetupTZData, ret); if (dataPtr->LastSetupTimeZone != timezoneObj) { Tcl_SetObjRef(dataPtr->LastSetupTimeZone, timezoneObj); Tcl_UnsetObjRef(dataPtr->LastUnnormSetupTimeZone); - } + } return ret; } /* @@ -710,49 +692,6 @@ ClockFormatNumericTimeZone(int z) { /* *---------------------------------------------------------------------- - * [SB] TODO: make constans cacheable (once per second, etc.) ... - */ -inline int -ClockCurrentYearCentury( - ClientData clientData, /* Opaque pointer to literal pool, etc. */ - Tcl_Interp *interp) /* Tcl interpreter */ -{ - ClockClientData *dataPtr = clientData; - Tcl_Obj **literals = dataPtr->literals; - int year = dataPtr->currentYearCentury; - - if (year == -1) { - Tcl_Obj * yearObj; - year = 2000; - yearObj = Tcl_ObjGetVar2(interp, - literals[LIT_CURRENTYEARCENTURY], NULL, TCL_LEAVE_ERR_MSG); - Tcl_GetIntFromObj(NULL, yearObj, &year); - dataPtr->currentYearCentury = year; - } - return year; -} -inline int -ClockGetYearOfCenturySwitch( - ClientData clientData, /* Opaque pointer to literal pool, etc. */ - Tcl_Interp *interp) /* Tcl interpreter */ -{ - ClockClientData *dataPtr = clientData; - Tcl_Obj **literals = dataPtr->literals; - int year = dataPtr->yearOfCenturySwitch; - - if (year == -1) { - Tcl_Obj * yearObj; - year = 37; - yearObj = Tcl_ObjGetVar2(interp, - literals[LIT_YEAROFCENTURYSWITCH], NULL, TCL_LEAVE_ERR_MSG); - Tcl_GetIntFromObj(NULL, yearObj, &year); - dataPtr->yearOfCenturySwitch = year; - } - return year; -} - -/* - *---------------------------------------------------------------------- * * ClockConvertlocaltoutcObjCmd -- * @@ -816,7 +755,7 @@ ClockConvertlocaltoutcObjCmd( if ((Tcl_GetWideIntFromObj(interp, secondsObj, &fields.localSeconds) != TCL_OK) || (TclGetIntFromObj(interp, objv[3], &changeover) != TCL_OK) - || ConvertLocalToUTC(interp, &fields, objv[2], changeover)) { + || ConvertLocalToUTC(clientData, interp, &fields, objv[2], changeover)) { return TCL_ERROR; } @@ -911,8 +850,8 @@ ClockGetdatefieldsObjCmd( /* Extract fields */ - if (ClockGetDateFields(interp, &fields, objv[2], changeover) - != TCL_OK) { + if (ClockGetDateFields(clientData, interp, &fields, objv[2], + changeover) != TCL_OK) { return TCL_ERROR; } @@ -957,6 +896,7 @@ ClockGetdatefieldsObjCmd( */ int ClockGetDateFields( + ClientData clientData, /* Client data of the interpreter */ Tcl_Interp *interp, /* Tcl interpreter */ TclDateFields *fields, /* Pointer to result fields, where * fields->seconds contains date to extract */ @@ -967,7 +907,8 @@ ClockGetDateFields( * Convert UTC time to local. */ - if (ConvertUTCToLocal(interp, fields, tzdata, changeover) != TCL_OK) { + if (ConvertUTCToLocal(clientData, interp, fields, tzdata, + changeover) != TCL_OK) { return TCL_ERROR; } @@ -1221,15 +1162,33 @@ ClockGetjuliandayfromerayearweekdayObjCmd( static int ConvertLocalToUTC( + ClientData clientData, /* Client data of the interpreter */ Tcl_Interp *interp, /* Tcl interpreter */ TclDateFields *fields, /* Fields of the time */ Tcl_Obj *tzdata, /* Time zone data */ int changeover) /* Julian Day of the Gregorian transition */ { + ClockClientData *dataPtr = clientData; int rowc; /* Number of rows in tzdata */ Tcl_Obj **rowv; /* Pointers to the rows */ /* + * Check cacheable conversion could be used + * (last-minute Local2UTC cache with the same TZ) + */ + if ( tzdata == dataPtr->Local2UTC.tzData + && ( fields->localSeconds == dataPtr->Local2UTC.localSeconds + || fields->localSeconds / 60 == dataPtr->Local2UTC.localSeconds / 60 + ) + && changeover == dataPtr->Local2UTC.changeover + ) { + /* the same time zone and offset (UTC time inside the last minute) */ + fields->tzOffset = dataPtr->Local2UTC.tzOffset; + fields->seconds = fields->localSeconds - fields->tzOffset; + return TCL_OK; + } + + /* * Unpack the tz data. */ @@ -1243,10 +1202,22 @@ ConvertLocalToUTC( */ if (rowc == 0) { - return ConvertLocalToUTCUsingC(interp, fields, changeover); + if (ConvertLocalToUTCUsingC(interp, fields, changeover) != TCL_OK) { + return TCL_ERROR; + }; } else { - return ConvertLocalToUTCUsingTable(interp, fields, rowc, rowv); + if (ConvertLocalToUTCUsingTable(interp, fields, rowc, rowv) != TCL_OK) { + return TCL_ERROR; + }; } + + /* Cache the last conversion */ + Tcl_SetObjRef(dataPtr->Local2UTC.tzData, tzdata); + dataPtr->Local2UTC.localSeconds = fields->localSeconds; + dataPtr->Local2UTC.changeover = changeover; + dataPtr->Local2UTC.tzOffset = fields->tzOffset; + + return TCL_OK; } /* @@ -1321,6 +1292,40 @@ ConvertLocalToUTCUsingTable( } fields->tzOffset = have[i]; fields->seconds = fields->localSeconds - fields->tzOffset; + +#if 0 + /* + * Convert back from UTC, if local times are different - wrong local time + * (local time seems to be in between DST-hole). + */ + if (fields->tzOffset) { + + int corrOffset; + Tcl_WideInt backCompVal; + /* check DST-hole interval contains UTC time */ + Tcl_GetWideIntFromObj(NULL, cellv[0], &backCompVal); + if ( fields->seconds >= backCompVal - fields->tzOffset + && fields->seconds <= backCompVal + fields->tzOffset + ) { + row = LookupLastTransition(interp, fields->seconds, rowc, rowv); + if (row == NULL || + TclListObjGetElements(interp, row, &cellc, &cellv) != TCL_OK || + TclGetIntFromObj(interp, cellv[1], &corrOffset) != TCL_OK) { + return TCL_ERROR; + } + if (fields->localSeconds != fields->seconds + corrOffset) { + Tcl_Panic("wrong local time %ld by LocalToUTC conversion," + " local time seems to be in between DST-hole", + fields->localSeconds); + /* correcting offset * / + fields->tzOffset -= corrOffset; + fields->seconds += fields->tzOffset; + */ + } + } + } +#endif + return TCL_OK; } @@ -1424,15 +1429,34 @@ ConvertLocalToUTCUsingC( static int ConvertUTCToLocal( + ClientData clientData, /* Client data of the interpreter */ Tcl_Interp *interp, /* Tcl interpreter */ TclDateFields *fields, /* Fields of the time */ Tcl_Obj *tzdata, /* Time zone data */ int changeover) /* Julian Day of the Gregorian transition */ { + ClockClientData *dataPtr = clientData; int rowc; /* Number of rows in tzdata */ Tcl_Obj **rowv; /* Pointers to the rows */ /* + * Check cacheable conversion could be used + * (last-minute UTC2Local cache with the same TZ) + */ + if ( tzdata == dataPtr->UTC2Local.tzData + && ( fields->seconds == dataPtr->UTC2Local.seconds + || fields->seconds / 60 == dataPtr->UTC2Local.seconds / 60 + ) + && changeover == dataPtr->UTC2Local.changeover + ) { + /* the same time zone and offset (UTC time inside the last minute) */ + Tcl_SetObjRef(fields->tzName, dataPtr->UTC2Local.tzName); + fields->tzOffset = dataPtr->UTC2Local.tzOffset; + fields->localSeconds = fields->seconds + fields->tzOffset; + return TCL_OK; + } + + /* * Unpack the tz data. */ @@ -1446,10 +1470,22 @@ ConvertUTCToLocal( */ if (rowc == 0) { - return ConvertUTCToLocalUsingC(interp, fields, changeover); + if (ConvertUTCToLocalUsingC(interp, fields, changeover) != TCL_OK) { + return TCL_ERROR; + } } else { - return ConvertUTCToLocalUsingTable(interp, fields, rowc, rowv); + if (ConvertUTCToLocalUsingTable(interp, fields, rowc, rowv) != TCL_OK) { + return TCL_ERROR; + } } + + /* Cache the last conversion */ + Tcl_SetObjRef(dataPtr->UTC2Local.tzData, tzdata); + dataPtr->UTC2Local.seconds = fields->seconds; + dataPtr->UTC2Local.changeover = changeover; + dataPtr->UTC2Local.tzOffset = fields->tzOffset; + Tcl_SetObjRef(dataPtr->UTC2Local.tzName, fields->tzName); + return TCL_OK; } /* @@ -2373,6 +2409,7 @@ _ClockParseFmtScnArgs( resOpts->localeObj = NULL; resOpts->timezoneObj = NULL; resOpts->baseObj = NULL; + resOpts->flags = 0; for (i = 2; i < objc; i+=2) { if (Tcl_GetIndexFromObj(interp, objv[i], options[forScan], "option", 0, &optionIndex) != TCL_OK) { @@ -2539,6 +2576,8 @@ ClockScanObjCmd( ret = TCL_ERROR; + info->flags = 0; + if (opts.baseObj != NULL) { if (Tcl_GetWideIntFromObj(interp, opts.baseObj, &baseVal) != TCL_OK) { return TCL_ERROR; @@ -2580,20 +2619,20 @@ ClockScanObjCmd( } Tcl_SetObjRef(yydate.tzName, opts.timezoneObj); - /* check cached */ - if ( dataPtr->lastBaseTimeZone == opts.timezoneObj - && dataPtr->lastBaseDate.seconds == baseVal) { - memcpy(&yydate, &dataPtr->lastBaseDate, ClockCacheableDateFieldsSize); + /* check base fields already cached (by TZ, last-second cache) */ + if ( dataPtr->lastBase.TimeZone == opts.timezoneObj + && dataPtr->lastBase.Date.seconds == baseVal) { + memcpy(&yydate, &dataPtr->lastBase.Date, ClockCacheableDateFieldsSize); } else { /* extact fields from base */ yydate.seconds = baseVal; - if (ClockGetDateFields(interp, &yydate, yydate.tzData, GREGORIAN_CHANGE_DATE) - != TCL_OK) { + if (ClockGetDateFields(clientData, interp, &yydate, yydate.tzData, + GREGORIAN_CHANGE_DATE) != TCL_OK) { goto done; } /* cache last base */ - memcpy(&dataPtr->lastBaseDate, &yydate, ClockCacheableDateFieldsSize); - dataPtr->lastBaseTimeZone = opts.timezoneObj; + memcpy(&dataPtr->lastBase.Date, &yydate, ClockCacheableDateFieldsSize); + Tcl_SetObjRef(dataPtr->lastBase.TimeZone, opts.timezoneObj); } /* seconds are in localSeconds (relative base date), so reset time here */ @@ -2612,7 +2651,7 @@ ClockScanObjCmd( } ret = ClockFreeScan(clientData, interp, info, objv[1], &opts); } -#if 0 +#if 1 else if (1) { /* TODO: Tcled Scan proc - */ @@ -2636,24 +2675,40 @@ ClockScanObjCmd( goto done; } - /* If needed assemble julianDay using new year, month, etc. */ - if (yydate.julianDay == CL_INVALIDATE) { + if (info->flags & CLF_INVALIDATE_JULIANDAY) { GetJulianDayFromEraYearMonthDay(&yydate, GREGORIAN_CHANGE_DATE); } - + + /* some overflow checks, if not extended */ + if (!(opts.flags & CLF_EXTENDED)) { + if (yydate.julianDay > 5373484) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "requested date too large to represent", -1)); + Tcl_SetErrorCode(interp, "CLOCK", "dateTooLarge", NULL); + ret = TCL_ERROR; + goto done; + } + } + /* Local seconds to UTC (stored in yydate.seconds) */ - yydate.localSeconds = - -210866803200L - + ( 86400 * (Tcl_WideInt)yydate.julianDay ) - + ( yySeconds % 86400 ); + if (info->flags & (CLF_INVALIDATE_SECONDS|CLF_INVALIDATE_JULIANDAY)) { + yydate.localSeconds = + -210866803200L + + ( SECONDS_PER_DAY * (Tcl_WideInt)yydate.julianDay ) + + ( yySeconds % SECONDS_PER_DAY ); + } - if (ConvertLocalToUTC(interp, &yydate, yydate.tzData, GREGORIAN_CHANGE_DATE) + if (ConvertLocalToUTC(clientData, interp, &yydate, yydate.tzData, GREGORIAN_CHANGE_DATE) != TCL_OK) { goto done; } + /* Increment UTC seconds with relative time */ + + yydate.seconds += yyRelSeconds; + ret = TCL_OK; done: @@ -2681,7 +2736,7 @@ ClockFreeScan( Tcl_Obj *strObj, /* String containing the time to scan */ ClockFmtScnCmdArgs *opts) /* Command options */ { - // ClockClientData *dataPtr = clientData; + ClockClientData *dataPtr = clientData; // Tcl_Obj **literals = dataPtr->literals; int ret = TCL_ERROR; @@ -2712,15 +2767,16 @@ ClockFreeScan( if (yyHaveDate) { if (yyYear < 100) { - if (yyYear >= ClockGetYearOfCenturySwitch(clientData, interp)) { + if (yyYear >= dataPtr->yearOfCenturySwitch) { yyYear -= 100; } - yyYear += ClockCurrentYearCentury(clientData, interp); + yyYear += dataPtr->currentYearCentury; } yydate.era = CE; if (yyHaveTime == 0) { yyHaveTime = -1; } + info->flags |= CLF_INVALIDATE_JULIANDAY|CLF_INVALIDATE_SECONDS; } /* @@ -2749,22 +2805,22 @@ ClockFreeScan( Tcl_SetObjRef(yydate.tzName, opts->timezoneObj); + info->flags |= CLF_INVALIDATE_SECONDS; } - /* on demand (lazy) assemble julianDay using new year, month, etc. */ - yydate.julianDay = CL_INVALIDATE; - /* * Assemble date, time, zone into seconds-from-epoch */ if (yyHaveTime == -1) { yySeconds = 0; + info->flags |= CLF_INVALIDATE_SECONDS; } else if (yyHaveTime) { yySeconds = ToSeconds(yyHour, yyMinutes, yySeconds, yyMeridian); + info->flags |= CLF_INVALIDATE_SECONDS; } else if ( (yyHaveDay && !yyHaveDate) @@ -2774,9 +2830,10 @@ ClockFreeScan( || yyRelDay != 0 ) ) ) { yySeconds = 0; + info->flags |= CLF_INVALIDATE_SECONDS; } else { - yySeconds = yydate.localSeconds % 86400; + yySeconds = yydate.localSeconds % SECONDS_PER_DAY; } /* @@ -2787,16 +2844,24 @@ repeat_rel: if (yyHaveRel) { + /* + * Relative conversion normally possible in UTC time only, because + * of possible wrong local time increment if ignores in-between DST-hole. + * (see test-cases clock-34.53, clock-34.54). + * So increment date in julianDay, but time inside day in UTC (seconds). + */ + /* add months (or years in months) */ if (yyRelMonth != 0) { int m, h; /* if needed extract year, month, etc. again */ - if (yyMonth == CL_INVALIDATE) { + if (info->flags & CLF_INVALIDATE_DATE) { GetGregorianEraYearDay(&yydate, GREGORIAN_CHANGE_DATE); GetMonthDay(&yydate); GetYearWeekDay(&yydate, GREGORIAN_CHANGE_DATE); + info->flags &= ~CLF_INVALIDATE_DATE; } /* add the requisite number of months */ @@ -2812,7 +2877,7 @@ repeat_rel: } /* on demand (lazy) assemble julianDay using new year, month, etc. */ - yydate.julianDay = CL_INVALIDATE; + info->flags |= CLF_INVALIDATE_JULIANDAY|CLF_INVALIDATE_SECONDS; yyRelMonth = 0; } @@ -2821,21 +2886,33 @@ repeat_rel: if (yyRelDay) { /* assemble julianDay using new year, month, etc. */ - if (yydate.julianDay == CL_INVALIDATE) { + if (info->flags & CLF_INVALIDATE_JULIANDAY) { GetJulianDayFromEraYearMonthDay(&yydate, GREGORIAN_CHANGE_DATE); + info->flags &= ~CLF_INVALIDATE_JULIANDAY; } - yydate.julianDay += yyRelDay; + yydate.julianDay += yyRelDay; /* julianDay was changed, on demand (lazy) extract year, month, etc. again */ - yyMonth = CL_INVALIDATE; + info->flags |= CLF_INVALIDATE_DATE|CLF_INVALIDATE_SECONDS; yyRelDay = 0; } - /* relative time (seconds) */ - yySeconds += yyRelSeconds; - yyRelSeconds = 0; - + /* relative time (seconds), if exceeds current date, do the day conversion and + * leave rest of the increment in yyRelSeconds to add it hereafter in UTC seconds */ + if (yyRelSeconds) { + time_t newSecs = yySeconds + yyRelSeconds; + + /* if seconds increment outside of current date, increment day */ + if (newSecs / SECONDS_PER_DAY != yySeconds / SECONDS_PER_DAY) { + + yyRelDay += newSecs / SECONDS_PER_DAY; + yySeconds = 0; + yyRelSeconds = newSecs % SECONDS_PER_DAY; + + goto repeat_rel; + } + } } /* @@ -2846,10 +2923,11 @@ repeat_rel: int monthDiff; /* if needed extract year, month, etc. again */ - if (yyMonth == CL_INVALIDATE) { + if (info->flags & CLF_INVALIDATE_DATE) { GetGregorianEraYearDay(&yydate, GREGORIAN_CHANGE_DATE); GetMonthDay(&yydate); GetYearWeekDay(&yydate, GREGORIAN_CHANGE_DATE); + info->flags &= ~CLF_INVALIDATE_DATE; } if (yyMonthOrdinalIncr > 0) { @@ -2872,6 +2950,8 @@ repeat_rel: yyRelMonth += monthDiff; yyHaveOrdinalMonth = 0; + info->flags |= CLF_INVALIDATE_JULIANDAY|CLF_INVALIDATE_SECONDS; + goto repeat_rel; } @@ -2882,8 +2962,9 @@ repeat_rel: if (yyHaveDay && !yyHaveDate) { /* if needed assemble julianDay now */ - if (yydate.julianDay == CL_INVALIDATE) { + if (info->flags & CLF_INVALIDATE_JULIANDAY) { GetJulianDayFromEraYearMonthDay(&yydate, GREGORIAN_CHANGE_DATE); + info->flags &= ~CLF_INVALIDATE_JULIANDAY; } yydate.era = CE; @@ -2892,6 +2973,7 @@ repeat_rel: if (yyDayOrdinal > 0) { yydate.julianDay -= 7; } + info->flags |= CLF_INVALIDATE_DATE|CLF_INVALIDATE_SECONDS; } /* Free scanning completed - date ready */ diff --git a/generic/tclClockFmt.c b/generic/tclClockFmt.c index b074681..48b9b69 100644 --- a/generic/tclClockFmt.c +++ b/generic/tclClockFmt.c @@ -35,6 +35,66 @@ static void ClockFmtScnStorageDelete(ClockFmtScnStorage *fss); * Clock scan and format facilities. */ +inline int +_str2int( + time_t *out, + register + const char *p, + const char *e, + int sign) +{ + register time_t val = 0, prev = 0; + if (sign > 0) { + while (p < e) { + val = val * 10 + (*p++ - '0'); + if (val < prev) { + return TCL_ERROR; + } + prev = val; + } + } else { + while (p < e) { + val = val * 10 - (*p++ - '0'); + if (val > prev) { + return TCL_ERROR; + } + prev = val; + } + } + *out = val; + return TCL_OK; +} + +inline int +_str2wideInt( + Tcl_WideInt *out, + register + const char *p, + const char *e, + int sign) +{ + register Tcl_WideInt val = 0, prev = 0; + if (sign > 0) { + while (p < e) { + val = val * 10 + (*p++ - '0'); + if (val < prev) { + return TCL_ERROR; + } + prev = val; + } + } else { + while (p < e) { + val = val * 10 - (*p++ - '0'); + if (val > prev) { + return TCL_ERROR; + } + prev = val; + } + } + *out = val; + return TCL_OK; +} + /* *---------------------------------------------------------------------- */ @@ -403,22 +463,35 @@ Tcl_GetClockFrmScnFromObj( memset(tok, 0, sizeof(*(tok))); const char *ScnSTokenMapChars = - "dmyYHMS"; + "dmyYHMSJs"; static ClockScanTokenMap ScnSTokenMap[] = { - {CTOKT_DIGIT, CLF_DATE, 1, 2, TclOffset(DateInfo, date.dayOfMonth), + /* %d */ + {CTOKT_DIGIT, CLF_DATE, 1, 2, TclOffset(DateInfo, date.dayOfMonth), NULL}, + /* %m */ {CTOKT_DIGIT, CLF_DATE, 1, 2, TclOffset(DateInfo, date.month), NULL}, + /* %y */ {CTOKT_DIGIT, CLF_DATE, 1, 2, TclOffset(DateInfo, date.year), NULL}, + /* %Y */ {CTOKT_DIGIT, CLF_DATE, 1, 4, TclOffset(DateInfo, date.year), NULL}, + /* %H */ {CTOKT_DIGIT, CLF_TIME, 1, 2, TclOffset(DateInfo, date.hour), NULL}, + /* %M */ {CTOKT_DIGIT, CLF_TIME, 1, 2, TclOffset(DateInfo, date.minutes), NULL}, + /* %S */ {CTOKT_DIGIT, CLF_TIME, 1, 2, TclOffset(DateInfo, date.secondOfDay), NULL}, + /* %J */ + {CTOKT_DIGIT, CLF_DATE | CLF_JULIANDAY, 1, 0xffff, TclOffset(DateInfo, date.julianDay), + NULL}, + /* %s */ + {CTOKT_DIGIT, CLF_LOCALSEC | CLF_SIGNED, 1, 0xffff, TclOffset(DateInfo, date.localSeconds), + NULL}, }; const char *ScnSpecTokenMapChars = " "; @@ -576,6 +649,7 @@ ClockScan( Tcl_Obj *strObj, /* String containing the time to scan */ ClockFmtScnCmdArgs *opts) /* Command options */ { + ClockClientData *dataPtr = clientData; ClockScanToken *tok; ClockScanTokenMap *map; register const char *p, *x, *end; @@ -590,46 +664,100 @@ ClockScan( yyMeridian = MER24; - /* bypass spaces at begin of string */ - p = TclGetString(strObj); end = p + strObj->length; - while (p < end && isspace(UCHAR(*p))) { - p++; + /* in strict mode - bypass spaces at begin / end only (not between tokens) */ + if (opts->flags & CLF_STRICT) { + while (p < end && isspace(UCHAR(*p))) { + p++; + } } info->dateStart = yyInput = p; /* parse string */ - for (; tok->map != NULL; yyInput = p, tok++) { + for (; tok->map != NULL; tok++) { map = tok->map; + /* bypass spaces at begin of input before parsing each token */ + if (!(opts->flags & CLF_STRICT) && map->type != CTOKT_SPACE) { + while (p < end && isspace(UCHAR(*p))) { + p++; + } + yyInput = p; + } switch (map->type) { case CTOKT_DIGIT: if (1) { - unsigned int val = 0; int size = map->maxSize; - /* greedy find digits (look forward), corresponding pre-calculated lookAhead */ - size += tok->lookAhead; - x = yyInput + size; - while (isdigit(UCHAR(*p)) && p < x) { p++; }; - /* consider reserved (lookAhead) for next tokens */ - p -= tok->lookAhead; + int sign = 1; + if (map->flags & CLF_SIGNED) { + if (*p == '+') { yyInput = ++p; } + else + if (*p == '-') { yyInput = ++p; sign = -1; }; + } + /* greedy find digits (look for forward digits consider spaces), + * corresponding pre-calculated lookAhead */ + if (size != map->minSize && tok->lookAhead) { + int spcnt = 0; + const char *pe; + size += tok->lookAhead; + x = p + size; if (x > end) { x = end; }; + pe = x; + while (p < x) { + if (isspace(UCHAR(*p))) { + if (pe > p) { pe = p; }; + if (x < end) x++; + p++; + spcnt++; + continue; + } + if (isdigit(UCHAR(*p))) { + p++; + continue; + } + break; + } + /* consider reserved (lookAhead) for next tokens */ + p -= tok->lookAhead + spcnt; + if (p > pe) { + p = pe; + } + } else { + x = p + size; if (x > end) { x = end; }; + while (isdigit(UCHAR(*p)) && p < x) { p++; }; + } size = p - yyInput; if (size < map->minSize) { /* missing input -> error */ - goto done; + goto error; } - /* string 2 number */ + /* string 2 number, put number into info structure by offset */ p = yyInput; x = p + size; - while (p < x) { - val = val * 10 + (*p++ - '0'); + if (!(map->flags & CLF_LOCALSEC)) { + if (_str2int((time_t *)(((char *)info) + map->offs), + p, x, sign) != TCL_OK) { + goto overflow; + } + p = x; + } else { + if (_str2wideInt((Tcl_WideInt *)(((char *)info) + map->offs), + p, x, sign) != TCL_OK) { + goto overflow; + } + p = x; } - /* put number into info by offset */ - *(time_t *)(((char *)info) + map->offs) = val; flags |= map->flags; } break; case CTOKT_SPACE: + /* at least one space in strict mode */ + if (opts->flags & CLF_STRICT) { + if (!isspace(UCHAR(*p))) { + /* unmatched -> error */ + goto error; + } + p++; + } while (p < end && isspace(UCHAR(*p))) { p++; } @@ -639,7 +767,7 @@ ClockScan( if (x == tok->tokWord.end) { /* single char word */ if (*p != *x) { /* no match -> error */ - goto done; + goto error; } p++; continue; @@ -648,7 +776,7 @@ ClockScan( while (p < end && x < tok->tokWord.end && *p++ == *x++) {}; if (x < tok->tokWord.end) { /* no match -> error */ - goto done; + goto error; } break; } @@ -661,43 +789,57 @@ ClockScan( /* check end was reached */ if (p < end) { /* something after last token - wrong format */ - goto done; + goto error; } /* invalidate result */ if (flags & CLF_DATE) { - yydate.julianDay = CL_INVALIDATE; + if (!(flags & CLF_JULIANDAY)) { + info->flags |= CLF_INVALIDATE_SECONDS|CLF_INVALIDATE_JULIANDAY; - if (yyYear < 100) { - if (yyYear >= ClockGetYearOfCenturySwitch(clientData, interp)) { - yyYear -= 100; + if (yyYear < 100) { + if (yyYear >= dataPtr->yearOfCenturySwitch) { + yyYear -= 100; + } + yyYear += dataPtr->currentYearCentury; } - yyYear += ClockCurrentYearCentury(clientData, interp); + yydate.era = CE; } - yydate.era = CE; - if (!(flags & CLF_TIME)) { + /* if date but no time - reset time */ + if (!(flags & (CLF_TIME|CLF_LOCALSEC))) { + info->flags |= CLF_INVALIDATE_SECONDS; yydate.localSeconds = 0; } } if (flags & CLF_TIME) { + info->flags |= CLF_INVALIDATE_SECONDS; yySeconds = ToSeconds(yyHour, yyMinutes, yySeconds, yyMeridian); - } else { - yySeconds = yydate.localSeconds % 86400; + } else + if (!(flags & CLF_LOCALSEC)) { + info->flags |= CLF_INVALIDATE_SECONDS; + yySeconds = yydate.localSeconds % SECONDS_PER_DAY; } ret = TCL_OK; + goto done; -done: +overflow: - if (ret != TCL_OK) { - Tcl_SetResult(interp, - "input string does not match supplied format", TCL_STATIC); - Tcl_SetErrorCode(interp, "CLOCK", "badInputString", NULL); - return ret; - } + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "integer value too large to represent", -1)); + Tcl_SetErrorCode(interp, "CLOCK", "integervalueTooLarge", NULL); + goto done; + +error: + + Tcl_SetResult(interp, + "input string does not match supplied format", TCL_STATIC); + Tcl_SetErrorCode(interp, "CLOCK", "badInputString", NULL); + +done: return ret; } diff --git a/generic/tclDate.h b/generic/tclDate.h index 4b0b47e..47f2a21 100644 --- a/generic/tclDate.h +++ b/generic/tclDate.h @@ -14,6 +14,28 @@ #define _TCLCLOCK_H /* + * Constants + */ + +#define JULIAN_DAY_POSIX_EPOCH 2440588 +#define GREGORIAN_CHANGE_DATE 2361222 +#define SECONDS_PER_DAY 86400 +#define JULIAN_SEC_POSIX_EPOCH (((Tcl_WideInt) JULIAN_DAY_POSIX_EPOCH) \ + * SECONDS_PER_DAY) +#define FOUR_CENTURIES 146097 /* days */ +#define JDAY_1_JAN_1_CE_JULIAN 1721424 +#define JDAY_1_JAN_1_CE_GREGORIAN 1721426 +#define ONE_CENTURY_GREGORIAN 36524 /* days */ +#define FOUR_YEARS 1461 /* days */ +#define ONE_YEAR 365 /* days */ + + +/* On demand (lazy) assemble flags */ +#define CLF_INVALIDATE_DATE (1 << 6) /* assemble year, month, etc. using julianDay */ +#define CLF_INVALIDATE_JULIANDAY (1 << 7) /* assemble julianDay using year, month, etc. */ +#define CLF_INVALIDATE_SECONDS (1 << 8) /* assemble localSeconds (and seconds at end) */ + +/* * Structure containing the fields used in [clock format] and [clock scan] */ @@ -52,6 +74,7 @@ typedef struct TclDateFields { typedef struct DateInfo { const char *dateStart; const char *dateInput; + int flags; TclDateFields date; @@ -116,19 +139,71 @@ typedef struct DateInfo { #define yyDigitCount (info->dateDigitCount) -enum {CL_INVALIDATE = (signed int)0x80000000}; /* * Structure containing the command arguments supplied to [clock format] and [clock scan] */ +#define CLF_EXTENDED (1 << 4) +#define CLF_STRICT (1 << 8) + typedef struct ClockFmtScnCmdArgs { Tcl_Obj *formatObj; /* Format */ Tcl_Obj *localeObj; /* Name of the locale where the time will be expressed. */ Tcl_Obj *timezoneObj; /* Default time zone in which the time will be expressed */ Tcl_Obj *baseObj; /* Base (scan only) */ + int flags; /* Flags control scanning */ } ClockFmtScnCmdArgs; /* + * Structure containing the client data for [clock] + */ + +typedef struct ClockClientData { + int refCount; /* Number of live references. */ + Tcl_Obj **literals; /* Pool of object literals. */ + /* Cache for current clock parameters, imparted via "configure" */ + unsigned long LastTZEpoch; + int currentYearCentury; + int yearOfCenturySwitch; + Tcl_Obj *SystemTimeZone; + Tcl_Obj *SystemSetupTZData; + Tcl_Obj *GMTSetupTimeZone; + Tcl_Obj *GMTSetupTZData; + Tcl_Obj *AnySetupTimeZone; + Tcl_Obj *AnySetupTZData; + Tcl_Obj *LastUnnormSetupTimeZone; + Tcl_Obj *LastSetupTimeZone; + Tcl_Obj *LastSetupTZData; + /* Cache for last base (last-second fast convert if base/tz not changed) */ + struct { + Tcl_Obj *TimeZone; + TclDateFields Date; + } lastBase; + /* Las-minute cache for fast UTC2Local conversion */ + struct { + /* keys */ + Tcl_Obj *tzData; + int changeover; + Tcl_WideInt seconds; + /* values */ + time_t tzOffset; + Tcl_Obj *tzName; + } UTC2Local; + /* Las-minute cache for fast Local2UTC conversion */ + struct { + /* keys */ + Tcl_Obj *tzData; + int changeover; + Tcl_WideInt localSeconds; + /* values */ + time_t tzOffset; + } Local2UTC; +} ClockClientData; + +#define ClockDefaultYearCentury 2000 +#define ClockDefaultCenturySwitch 38 + +/* * Meridian: am, pm, or 24-hour style. */ @@ -152,8 +227,11 @@ typedef int ClockScanTokenProc( ClockScanToken *tok); -#define CLF_DATE (1 << 2) -#define CLF_TIME (1 << 3) +#define CLF_DATE (1 << 2) +#define CLF_JULIANDAY (1 << 3) +#define CLF_TIME (1 << 4) +#define CLF_LOCALSEC (1 << 5) +#define CLF_SIGNED (1 << 8) typedef enum _CLCKTOK_TYPE { CTOKT_DIGIT = 1, CTOKT_SPACE, CTOKT_WORD diff --git a/library/clock.tcl b/library/clock.tcl index ace4176..e0de904 100755 --- a/library/clock.tcl +++ b/library/clock.tcl @@ -287,9 +287,10 @@ proc ::tcl::clock::Initialize {} { variable FEB_28 58 - # Current year century and year of century switch - variable CurrentYearCentury 2000 - variable YearOfCenturySwitch 38 + # Default configuration + + # configure -year-century 2000 \ + # -century-switch 38 # Translation table to map Windows TZI onto cities, so that the Olson # rules can apply. In some cases the mapping is ambiguous, so it's wise @@ -2534,13 +2535,11 @@ proc ::tcl::clock::ScanWide { str } { proc ::tcl::clock::InterpretTwoDigitYear { date baseTime { twoDigitField yearOfCentury } { fourDigitField year } } { - variable CurrentYearCentury - variable YearOfCenturySwitch set yr [dict get $date $twoDigitField] - if { $yr >= $YearOfCenturySwitch } { + if { $yr >= [configure -century-switch] } { incr yr -100 } - incr yr $CurrentYearCentury + incr yr [configure -year-century] dict set date $fourDigitField $yr return $date } diff --git a/tests-perf/clock.perf.tcl b/tests-perf/clock.perf.tcl index 93a78e4..9267684 100644 --- a/tests-perf/clock.perf.tcl +++ b/tests-perf/clock.perf.tcl @@ -15,10 +15,19 @@ # of this file. # + +## set testing defaults: set ::env(TCL_TZ) :CET +## warm-up (load clock.tcl, system zones, etc.): +clock scan "" -gmt 1 +clock scan "" +clock scan "" -timezone :CET + +## ------------------------------------------ + proc {**STOP**} {args} { - return -code error -level 2 "**STOP** in [info level [expr {[info level]-1}]] [join $args { }]" + return -code error -level 4 "**STOP** in [info level [expr {[info level]-2}]] [join $args { }]" } proc _test_get_commands {lst} { @@ -43,7 +52,7 @@ proc _test_out_total {} { puts "" } -proc _test_run {reptime lst {outcmd {puts {$_(r)}}}} { +proc _test_run {reptime lst {outcmd {puts $_(r)}}} { upvar _ _ array set _ [list ittm {} itcnt {} itrate {} reptime $reptime] @@ -74,6 +83,22 @@ proc test-scan {{reptime 1000}} { {clock scan "1111" -format "%d%m%y" -base 0 -gmt 1} {clock scan "11111" -format "%d%m%y" -base 0 -gmt 1} {clock scan "111111" -format "%d%m%y" -base 0 -gmt 1} + # Scan : greedy match (space separated) + {clock scan "1 1 1" -format "%d%m%y" -base 0 -gmt 1} + {clock scan "111 1" -format "%d%m%y" -base 0 -gmt 1} + {clock scan "1 111" -format "%d%m%y" -base 0 -gmt 1} + {clock scan "1 11 1" -format "%d%m%y" -base 0 -gmt 1} + {clock scan "1 11 11" -format "%d%m%y" -base 0 -gmt 1} + {clock scan "11 11 11" -format "%d%m%y" -base 0 -gmt 1} + + # Scan : time (in gmt) + {clock scan "10:35:55" -format "%H:%M:%S" -base 1000000000 -gmt 1} + # Scan : time (system time zone, with base) + {clock scan "10:35:55" -format "%H:%M:%S" -base 1000000000} + # Scan : time (gmt, without base) + {clock scan "10:35:55" -format "%H:%M:%S" -gmt 1} + # Scan : time (system time zone, without base) + {clock scan "10:35:55" -format "%H:%M:%S"} # Scan : date-time (in gmt) {clock scan "25.11.2015 10:35:55" -format "%d.%m.%Y %H:%M:%S" -base 0 -gmt 1} @@ -85,6 +110,17 @@ proc test-scan {{reptime 1000}} { # Scan : dynamic format (cacheable) {clock scan "25.11.2015 10:35:55" -format [string trim "%d.%m.%Y %H:%M:%S "] -base 0 -gmt 1} + # Scan : julian day in gmt + {clock scan 2451545 -format %J -gmt 1} + # Scan : julian day in system TZ + {clock scan 2451545 -format %J} + # Scan : julian day in other TZ + {clock scan 2451545 -format %J -timezone +0200} + # Scan : julian day with time: + {clock scan "2451545 10:20:30" -format "%J %H:%M:%S"} + # Scan : julian day with time (greedy match): + {clock scan "2451545 102030" -format "%J%H%M%S"} + # Scan : zone only {clock scan "CET" -format "%z"} {clock scan "EST" -format "%z"} @@ -144,6 +180,10 @@ proc test-other {{reptime 1000}} { _test_run $reptime { # Bad zone {catch {clock scan "1 day" -timezone BAD_ZONE -locale en}} + + # Scan : julian day (overflow) + {catch {clock scan 5373485 -format %J}} + **STOP** # Scan : test rotate of GC objects (format is dynamic, so tcl-obj removed with last reference) {set i 0; time { clock scan "[incr i] - 25.11.2015" -format "$i - %d.%m.%Y" -base 0 -gmt 1 } 50} @@ -154,11 +194,11 @@ proc test-other {{reptime 1000}} { proc test {{reptime 1000}} { puts "" - test-scan $reptime - #test-freescan $reptime + #test-scan $reptime + test-freescan $reptime test-other $reptime puts \n**OK** } -test 250; # 250 ms +test 100; # ms diff --git a/tests/clock.test b/tests/clock.test index 477f142..9e3dbd7 100644 --- a/tests/clock.test +++ b/tests/clock.test @@ -35917,7 +35917,94 @@ test clock-34.52 {more than one ordinal month} {*}{ -result {unable to convert date-time string "next January next March": more than one ordinal month in string} } +test clock-34.53.1 {relative from base, date switch} { + set base [clock scan "12/31/2016 23:59:59" -gmt 1] + clock format [clock scan "+1 second" \ + -base $base -gmt 1] -gmt 1 -format {%Y-%m-%d %H:%M:%S} +} {2017-01-01 00:00:00} +test clock-34.53.2 {relative time, daylight switch} { + set base [clock scan "03/27/2016" -timezone CET] + set res {} + lappend res [clock format [clock scan "+1 hour" \ + -base $base -timezone CET] -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}] + lappend res [clock format [clock scan "+2 hour" \ + -base $base -timezone CET] -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}] +} {{2016-03-27 01:00:00 CET} {2016-03-27 03:00:00 CEST}} + +test clock-34.53.3 {relative time with day increment / daylight switch} { + set base [clock scan "03/27/2016" -timezone CET] + set res {} + lappend res [clock format [clock scan "+5 day +25 hour" \ + -base [expr {$base - 6*24*60*60}] -timezone CET] -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}] + lappend res [clock format [clock scan "+5 day +26 hour" \ + -base [expr {$base - 6*24*60*60}] -timezone CET] -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}] +} {{2016-03-27 01:00:00 CET} {2016-03-27 03:00:00 CEST}} + +test clock-34.53.4 {relative time with month & day increment / daylight switch} { + set base [clock scan "03/27/2016" -timezone CET] + set res {} + lappend res [clock format [clock scan "next Mar +5 day +25 hour" \ + -base [expr {$base - 35*24*60*60}] -timezone CET] -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}] + lappend res [clock format [clock scan "next Mar +5 day +26 hour" \ + -base [expr {$base - 35*24*60*60}] -timezone CET] -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}] +} {{2016-03-27 01:00:00 CET} {2016-03-27 03:00:00 CEST}} + +test clock-34.54.1 {check date in DST-hole: daylight switch CET -> CEST} { + set res {} + # forwards + set base 1459033200 + for {set i 0} {$i <= 3} {incr i} { + set d [clock scan "+$i hour" -base $base -timezone CET] + lappend res "$d = [clock format $d -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}]" + } + lappend res "#--" + # backwards + set base 1459044000 + for {set i 0} {$i <= 3} {incr i} { + set d [clock scan "-$i hour" -base $base -timezone CET] + lappend res "$d = [clock format $d -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}]" + } + set res +} [split [regsub -all {^\n|\n$} { +1459033200 = 2016-03-27 00:00:00 CET +1459036800 = 2016-03-27 01:00:00 CET +1459040400 = 2016-03-27 03:00:00 CEST +1459044000 = 2016-03-27 04:00:00 CEST +#-- +1459044000 = 2016-03-27 04:00:00 CEST +1459040400 = 2016-03-27 03:00:00 CEST +1459036800 = 2016-03-27 01:00:00 CET +1459033200 = 2016-03-27 00:00:00 CET +} {}] \n] + +test clock-34.54.2 {check date in DST-hole: daylight switch CEST -> CET} { + set res {} + # forwards + set base 1477782000 + for {set i 0} {$i <= 3} {incr i} { + set d [clock scan "+$i hour" -base $base -timezone CET] + lappend res "$d = [clock format $d -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}]" + } + lappend res "#--" + # backwards + set base 1477792800 + for {set i 0} {$i <= 3} {incr i} { + set d [clock scan "-$i hour" -base $base -timezone CET] + lappend res "$d = [clock format $d -timezone CET -format {%Y-%m-%d %H:%M:%S %Z}]" + } + set res +} [split [regsub -all {^\n|\n$} { +1477782000 = 2016-10-30 01:00:00 CEST +1477785600 = 2016-10-30 02:00:00 CEST +1477789200 = 2016-10-30 02:00:00 CET +1477792800 = 2016-10-30 03:00:00 CET +#-- +1477792800 = 2016-10-30 03:00:00 CET +1477789200 = 2016-10-30 02:00:00 CET +1477785600 = 2016-10-30 02:00:00 CEST +1477782000 = 2016-10-30 01:00:00 CEST +} {}] \n] # clock seconds test clock-35.1 {clock seconds tests} { -- cgit v0.12