Update tcl to version 8.5.9
[msysgit.git] / mingw / lib / tcl8.5 / clock.tcl
blob4e7df3130353e5a9ca96a15ed66748581c56c7c2
1 #----------------------------------------------------------------------
3 # clock.tcl --
5 # This file implements the portions of the [clock] ensemble that
6 # are coded in Tcl. Refer to the users' manual to see the description
7 # of the [clock] command and its subcommands.
10 #----------------------------------------------------------------------
12 # Copyright (c) 2004,2005,2006,2007 by Kevin B. Kenny
13 # See the file "license.terms" for information on usage and redistribution
14 # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
16 # RCS: @(#) $Id: clock.tcl,v 1.47.2.9 2009/10/29 01:17:03 kennykb Exp $
18 #----------------------------------------------------------------------
20 # We must have message catalogs that support the root locale, and
21 # we need access to the Registry on Windows systems.
23 uplevel \#0 {
24 package require msgcat 1.4
25 if { $::tcl_platform(platform) eq {windows} } {
26 if { [catch { package require registry 1.1 }] } {
27 namespace eval ::tcl::clock [list variable NoRegistry {}]
32 # Put the library directory into the namespace for the ensemble
33 # so that the library code can find message catalogs and time zone
34 # definition files.
36 namespace eval ::tcl::clock \
37 [list variable LibDir [file dirname [info script]]]
39 #----------------------------------------------------------------------
41 # clock --
43 # Manipulate times.
45 # The 'clock' command manipulates time. Refer to the user documentation
46 # for the available subcommands and what they do.
48 #----------------------------------------------------------------------
50 namespace eval ::tcl::clock {
52 # Export the subcommands
54 namespace export format
55 namespace export clicks
56 namespace export microseconds
57 namespace export milliseconds
58 namespace export scan
59 namespace export seconds
60 namespace export add
62 # Import the message catalog commands that we use.
64 namespace import ::msgcat::mcload
65 namespace import ::msgcat::mclocale
69 #----------------------------------------------------------------------
71 # ::tcl::clock::Initialize --
73 # Finish initializing the 'clock' subsystem
75 # Results:
76 # None.
78 # Side effects:
79 # Namespace variable in the 'clock' subsystem are initialized.
81 # The '::tcl::clock::Initialize' procedure initializes the namespace
82 # variables and root locale message catalog for the 'clock' subsystem.
83 # It is broken into a procedure rather than simply evaluated as a script
84 # so that it will be able to use local variables, avoiding the dangers
85 # of 'creative writing' as in Bug 1185933.
87 #----------------------------------------------------------------------
89 proc ::tcl::clock::Initialize {} {
91 rename ::tcl::clock::Initialize {}
93 variable LibDir
95 # Define the Greenwich time zone
97 proc InitTZData {} {
98 variable TZData
99 array unset TZData
100 set TZData(:Etc/GMT) {
101 {-9223372036854775808 0 0 GMT}
103 set TZData(:GMT) $TZData(:Etc/GMT)
104 set TZData(:Etc/UTC) {
105 {-9223372036854775808 0 0 UTC}
107 set TZData(:UTC) $TZData(:Etc/UTC)
108 set TZData(:localtime) {}
110 InitTZData
112 # Define the message catalog for the root locale.
114 ::msgcat::mcmset {} {
115 AM {am}
116 BCE {B.C.E.}
117 CE {C.E.}
118 DATE_FORMAT {%m/%d/%Y}
119 DATE_TIME_FORMAT {%a %b %e %H:%M:%S %Y}
120 DAYS_OF_WEEK_ABBREV {
121 Sun Mon Tue Wed Thu Fri Sat
123 DAYS_OF_WEEK_FULL {
124 Sunday Monday Tuesday Wednesday Thursday Friday Saturday
126 GREGORIAN_CHANGE_DATE 2299161
127 LOCALE_DATE_FORMAT {%m/%d/%Y}
128 LOCALE_DATE_TIME_FORMAT {%a %b %e %H:%M:%S %Y}
129 LOCALE_ERAS {}
130 LOCALE_NUMERALS {
131 00 01 02 03 04 05 06 07 08 09
132 10 11 12 13 14 15 16 17 18 19
133 20 21 22 23 24 25 26 27 28 29
134 30 31 32 33 34 35 36 37 38 39
135 40 41 42 43 44 45 46 47 48 49
136 50 51 52 53 54 55 56 57 58 59
137 60 61 62 63 64 65 66 67 68 69
138 70 71 72 73 74 75 76 77 78 79
139 80 81 82 83 84 85 86 87 88 89
140 90 91 92 93 94 95 96 97 98 99
142 LOCALE_TIME_FORMAT {%H:%M:%S}
143 LOCALE_YEAR_FORMAT {%EC%Ey}
144 MONTHS_ABBREV {
145 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
147 MONTHS_FULL {
148 January February March
149 April May June
150 July August September
151 October November December
153 PM {pm}
154 TIME_FORMAT {%H:%M:%S}
155 TIME_FORMAT_12 {%I:%M:%S %P}
156 TIME_FORMAT_24 {%H:%M}
157 TIME_FORMAT_24_SECS {%H:%M:%S}
160 # Define a few Gregorian change dates for other locales. In most cases
161 # the change date follows a language, because a nation's colonies changed
162 # at the same time as the nation itself. In many cases, different
163 # national boundaries existed; the dominating rule is to follow the
164 # nation's capital.
166 # Italy, Spain, Portugal, Poland
168 ::msgcat::mcset it GREGORIAN_CHANGE_DATE 2299161
169 ::msgcat::mcset es GREGORIAN_CHANGE_DATE 2299161
170 ::msgcat::mcset pt GREGORIAN_CHANGE_DATE 2299161
171 ::msgcat::mcset pl GREGORIAN_CHANGE_DATE 2299161
173 # France, Austria
175 ::msgcat::mcset fr GREGORIAN_CHANGE_DATE 2299227
177 # For Belgium, we follow Southern Netherlands; Liege Diocese
178 # changed several weeks later.
180 ::msgcat::mcset fr_BE GREGORIAN_CHANGE_DATE 2299238
181 ::msgcat::mcset nl_BE GREGORIAN_CHANGE_DATE 2299238
183 # Austria
185 ::msgcat::mcset de_AT GREGORIAN_CHANGE_DATE 2299527
187 # Hungary
189 ::msgcat::mcset hu GREGORIAN_CHANGE_DATE 2301004
191 # Germany, Norway, Denmark (Catholic Germany changed earlier)
193 ::msgcat::mcset de_DE GREGORIAN_CHANGE_DATE 2342032
194 ::msgcat::mcset nb GREGORIAN_CHANGE_DATE 2342032
195 ::msgcat::mcset nn GREGORIAN_CHANGE_DATE 2342032
196 ::msgcat::mcset no GREGORIAN_CHANGE_DATE 2342032
197 ::msgcat::mcset da GREGORIAN_CHANGE_DATE 2342032
199 # Holland (Brabant, Gelderland, Flanders, Friesland, etc. changed
200 # at various times)
202 ::msgcat::mcset nl GREGORIAN_CHANGE_DATE 2342165
204 # Protestant Switzerland (Catholic cantons changed earlier)
206 ::msgcat::mcset fr_CH GREGORIAN_CHANGE_DATE 2361342
207 ::msgcat::mcset it_CH GREGORIAN_CHANGE_DATE 2361342
208 ::msgcat::mcset de_CH GREGORIAN_CHANGE_DATE 2361342
210 # English speaking countries
212 ::msgcat::mcset en GREGORIAN_CHANGE_DATE 2361222
214 # Sweden (had several changes onto and off of the Gregorian calendar)
216 ::msgcat::mcset sv GREGORIAN_CHANGE_DATE 2361390
218 # Russia
220 ::msgcat::mcset ru GREGORIAN_CHANGE_DATE 2421639
222 # Romania (Transylvania changed earler - perhaps de_RO should show
223 # the earlier date?)
225 ::msgcat::mcset ro GREGORIAN_CHANGE_DATE 2422063
227 # Greece
229 ::msgcat::mcset el GREGORIAN_CHANGE_DATE 2423480
231 #------------------------------------------------------------------
233 # CONSTANTS
235 #------------------------------------------------------------------
237 # Paths at which binary time zone data for the Olson libraries
238 # are known to reside on various operating systems
240 variable ZoneinfoPaths {}
241 foreach path {
242 /usr/share/zoneinfo
243 /usr/share/lib/zoneinfo
244 /usr/lib/zoneinfo
245 /usr/local/etc/zoneinfo
247 if { [file isdirectory $path] } {
248 lappend ZoneinfoPaths $path
252 # Define the directories for time zone data and message catalogs.
254 variable DataDir [file join $LibDir tzdata]
255 variable MsgDir [file join $LibDir msgs]
257 # Number of days in the months, in common years and leap years.
259 variable DaysInRomanMonthInCommonYear \
260 { 31 28 31 30 31 30 31 31 30 31 30 31 }
261 variable DaysInRomanMonthInLeapYear \
262 { 31 29 31 30 31 30 31 31 30 31 30 31 }
263 variable DaysInPriorMonthsInCommonYear [list 0]
264 variable DaysInPriorMonthsInLeapYear [list 0]
265 set i 0
266 foreach j $DaysInRomanMonthInCommonYear {
267 lappend DaysInPriorMonthsInCommonYear [incr i $j]
269 set i 0
270 foreach j $DaysInRomanMonthInLeapYear {
271 lappend DaysInPriorMonthsInLeapYear [incr i $j]
274 # Another epoch (Hi, Jeff!)
276 variable Roddenberry 1946
278 # Integer ranges
280 variable MINWIDE -9223372036854775808
281 variable MAXWIDE 9223372036854775807
283 # Day before Leap Day
285 variable FEB_28 58
287 # Translation table to map Windows TZI onto cities, so that
288 # the Olson rules can apply. In some cases the mapping is ambiguous,
289 # so it's wise to specify $::env(TCL_TZ) rather than simply depending
290 # on the system time zone.
292 # The keys are long lists of values obtained from the time zone
293 # information in the Registry. In order, the list elements are:
294 # Bias StandardBias DaylightBias
295 # StandardDate.wYear StandardDate.wMonth StandardDate.wDayOfWeek
296 # StandardDate.wDay StandardDate.wHour StandardDate.wMinute
297 # StandardDate.wSecond StandardDate.wMilliseconds
298 # DaylightDate.wYear DaylightDate.wMonth DaylightDate.wDayOfWeek
299 # DaylightDate.wDay DaylightDate.wHour DaylightDate.wMinute
300 # DaylightDate.wSecond DaylightDate.wMilliseconds
301 # The values are the names of time zones where those rules apply.
302 # There is considerable ambiguity in certain zones; an attempt has
303 # been made to make a reasonable guess, but this table needs to be
304 # taken with a grain of salt.
306 variable WinZoneInfo [dict create {*}{
307 {-43200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Kwajalein
308 {-39600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Midway
309 {-36000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Honolulu
310 {-32400 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Anchorage
311 {-28800 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Los_Angeles
312 {-28800 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Tijuana
313 {-25200 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Denver
314 {-25200 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Chihuahua
315 {-25200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Phoenix
316 {-21600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Regina
317 {-21600 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Chicago
318 {-21600 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Mexico_City
319 {-18000 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/New_York
320 {-18000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Indianapolis
321 {-14400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Caracas
322 {-14400 0 3600 0 3 6 2 23 59 59 999 0 10 6 2 23 59 59 999}
323 :America/Santiago
324 {-14400 0 3600 0 2 0 5 2 0 0 0 0 11 0 1 2 0 0 0} :America/Manaus
325 {-14400 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Halifax
326 {-12600 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/St_Johns
327 {-10800 0 3600 0 2 0 2 2 0 0 0 0 10 0 3 2 0 0 0} :America/Sao_Paulo
328 {-10800 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Godthab
329 {-10800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Buenos_Aires
330 {-10800 0 3600 0 2 0 5 2 0 0 0 0 11 0 1 2 0 0 0} :America/Brasilia
331 {-10800 0 3600 0 3 0 2 2 0 0 0 0 10 0 1 2 0 0 0} :America/Montevideo
332 {-7200 0 3600 0 9 0 5 2 0 0 0 0 3 0 5 2 0 0 0} :America/Noronha
333 {-3600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Atlantic/Azores
334 {-3600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Atlantic/Cape_Verde
335 {0 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :UTC
336 {0 0 3600 0 10 0 5 2 0 0 0 0 3 0 5 1 0 0 0} :Europe/London
337 {3600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Africa/Kinshasa
338 {3600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :CET
339 {7200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Africa/Harare
340 {7200 0 3600 0 9 4 5 23 59 59 0 0 4 4 5 23 59 59 0}
341 :Africa/Cairo
342 {7200 0 3600 0 10 0 5 4 0 0 0 0 3 0 5 3 0 0 0} :Europe/Helsinki
343 {7200 0 3600 0 9 0 3 2 0 0 0 0 3 5 5 2 0 0 0} :Asia/Jerusalem
344 {7200 0 3600 0 9 0 5 1 0 0 0 0 3 0 5 0 0 0 0} :Europe/Bucharest
345 {7200 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Europe/Athens
346 {7200 0 3600 0 9 5 5 1 0 0 0 0 3 4 5 0 0 0 0} :Asia/Amman
347 {7200 0 3600 0 10 6 5 23 59 59 999 0 3 0 5 0 0 0 0}
348 :Asia/Beirut
349 {7200 0 -3600 0 4 0 1 2 0 0 0 0 9 0 1 2 0 0 0} :Africa/Windhoek
350 {10800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Riyadh
351 {10800 0 3600 0 10 0 1 4 0 0 0 0 4 0 1 3 0 0 0} :Asia/Baghdad
352 {10800 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Europe/Moscow
353 {12600 0 3600 0 9 2 4 2 0 0 0 0 3 0 1 2 0 0 0} :Asia/Tehran
354 {14400 0 3600 0 10 0 5 5 0 0 0 0 3 0 5 4 0 0 0} :Asia/Baku
355 {14400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Muscat
356 {14400 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Tbilisi
357 {16200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Kabul
358 {18000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Karachi
359 {18000 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Yekaterinburg
360 {19800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Calcutta
361 {20700 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Katmandu
362 {21600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Dhaka
363 {21600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Novosibirsk
364 {23400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Rangoon
365 {25200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Bangkok
366 {25200 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Krasnoyarsk
367 {28800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Chongqing
368 {28800 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Irkutsk
369 {32400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Tokyo
370 {32400 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Yakutsk
371 {34200 0 3600 0 3 0 5 3 0 0 0 0 10 0 5 2 0 0 0} :Australia/Adelaide
372 {34200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Australia/Darwin
373 {36000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Australia/Brisbane
374 {36000 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Vladivostok
375 {36000 0 3600 0 3 0 5 3 0 0 0 0 10 0 1 2 0 0 0} :Australia/Hobart
376 {36000 0 3600 0 3 0 5 3 0 0 0 0 10 0 5 2 0 0 0} :Australia/Sydney
377 {39600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Noumea
378 {43200 0 3600 0 3 0 3 3 0 0 0 0 10 0 1 2 0 0 0} :Pacific/Auckland
379 {43200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Fiji
380 {46800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Tongatapu
383 # Groups of fields that specify the date, priorities, and
384 # code bursts that determine Julian Day Number given those groups.
385 # The code in [clock scan] will choose the highest priority
386 # (lowest numbered) set of fields that determines the date.
388 variable DateParseActions {
390 { seconds } 0 {}
392 { julianDay } 1 {}
394 { era century yearOfCentury month dayOfMonth } 2 {
395 dict set date year [expr { 100 * [dict get $date century]
396 + [dict get $date yearOfCentury] }]
397 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
398 $changeover]
400 { era century yearOfCentury dayOfYear } 2 {
401 dict set date year [expr { 100 * [dict get $date century]
402 + [dict get $date yearOfCentury] }]
403 set date [GetJulianDayFromEraYearDay $date[set date {}] \
404 $changeover]
407 { century yearOfCentury month dayOfMonth } 3 {
408 dict set date era CE
409 dict set date year [expr { 100 * [dict get $date century]
410 + [dict get $date yearOfCentury] }]
411 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
412 $changeover]
414 { century yearOfCentury dayOfYear } 3 {
415 dict set date era CE
416 dict set date year [expr { 100 * [dict get $date century]
417 + [dict get $date yearOfCentury] }]
418 set date [GetJulianDayFromEraYearDay $date[set date {}] \
419 $changeover]
421 { iso8601Century iso8601YearOfCentury iso8601Week dayOfWeek } 3 {
422 dict set date era CE
423 dict set date iso8601Year \
424 [expr { 100 * [dict get $date iso8601Century]
425 + [dict get $date iso8601YearOfCentury] }]
426 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
427 $changeover]
430 { yearOfCentury month dayOfMonth } 4 {
431 set date [InterpretTwoDigitYear $date[set date {}] $baseTime]
432 dict set date era CE
433 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
434 $changeover]
436 { yearOfCentury dayOfYear } 4 {
437 set date [InterpretTwoDigitYear $date[set date {}] $baseTime]
438 dict set date era CE
439 set date [GetJulianDayFromEraYearDay $date[set date {}] \
440 $changeover]
442 { iso8601YearOfCentury iso8601Week dayOfWeek } 4 {
443 set date [InterpretTwoDigitYear \
444 $date[set date {}] $baseTime \
445 iso8601YearOfCentury iso8601Year]
446 dict set date era CE
447 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
448 $changeover]
451 { month dayOfMonth } 5 {
452 set date [AssignBaseYear $date[set date {}] \
453 $baseTime $timeZone $changeover]
454 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
455 $changeover]
457 { dayOfYear } 5 {
458 set date [AssignBaseYear $date[set date {}] \
459 $baseTime $timeZone $changeover]
460 set date [GetJulianDayFromEraYearDay $date[set date {}] \
461 $changeover]
463 { iso8601Week dayOfWeek } 5 {
464 set date [AssignBaseIso8601Year $date[set date {}] \
465 $baseTime $timeZone $changeover]
466 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
467 $changeover]
470 { dayOfMonth } 6 {
471 set date [AssignBaseMonth $date[set date {}] \
472 $baseTime $timeZone $changeover]
473 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
474 $changeover]
477 { dayOfWeek } 7 {
478 set date [AssignBaseWeek $date[set date {}] \
479 $baseTime $timeZone $changeover]
480 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
481 $changeover]
484 {} 8 {
485 set date [AssignBaseJulianDay $date[set date {}] \
486 $baseTime $timeZone $changeover]
490 # Groups of fields that specify time of day, priorities,
491 # and code that processes them
493 variable TimeParseActions {
495 seconds 1 {}
497 { hourAMPM minute second amPmIndicator } 2 {
498 dict set date secondOfDay [InterpretHMSP $date]
500 { hour minute second } 2 {
501 dict set date secondOfDay [InterpretHMS $date]
504 { hourAMPM minute amPmIndicator } 3 {
505 dict set date second 0
506 dict set date secondOfDay [InterpretHMSP $date]
508 { hour minute } 3 {
509 dict set date second 0
510 dict set date secondOfDay [InterpretHMS $date]
513 { hourAMPM amPmIndicator } 4 {
514 dict set date minute 0
515 dict set date second 0
516 dict set date secondOfDay [InterpretHMSP $date]
518 { hour } 4 {
519 dict set date minute 0
520 dict set date second 0
521 dict set date secondOfDay [InterpretHMS $date]
524 { } 5 {
525 dict set date secondOfDay 0
529 # Legacy time zones, used primarily for parsing RFC822 dates.
531 variable LegacyTimeZone [dict create \
532 gmt +0000 \
533 ut +0000 \
534 utc +0000 \
535 bst +0100 \
536 wet +0000 \
537 wat -0100 \
538 at -0200 \
539 nft -0330 \
540 nst -0330 \
541 ndt -0230 \
542 ast -0400 \
543 adt -0300 \
544 est -0500 \
545 edt -0400 \
546 cst -0600 \
547 cdt -0500 \
548 mst -0700 \
549 mdt -0600 \
550 pst -0800 \
551 pdt -0700 \
552 yst -0900 \
553 ydt -0800 \
554 hst -1000 \
555 hdt -0900 \
556 cat -1000 \
557 ahst -1000 \
558 nt -1100 \
559 idlw -1200 \
560 cet +0100 \
561 cest +0200 \
562 met +0100 \
563 mewt +0100 \
564 mest +0200 \
565 swt +0100 \
566 sst +0200 \
567 fwt +0100 \
568 fst +0200 \
569 eet +0200 \
570 eest +0300 \
571 bt +0300 \
572 it +0330 \
573 zp4 +0400 \
574 zp5 +0500 \
575 ist +0530 \
576 zp6 +0600 \
577 wast +0700 \
578 wadt +0800 \
579 jt +0730 \
580 cct +0800 \
581 jst +0900 \
582 kst +0900 \
583 cast +0930 \
584 jdt +1000 \
585 kdt +1000 \
586 cadt +1030 \
587 east +1000 \
588 eadt +1030 \
589 gst +1000 \
590 nzt +1200 \
591 nzst +1200 \
592 nzdt +1300 \
593 idle +1200 \
594 a +0100 \
595 b +0200 \
596 c +0300 \
597 d +0400 \
598 e +0500 \
599 f +0600 \
600 g +0700 \
601 h +0800 \
602 i +0900 \
603 k +1000 \
604 l +1100 \
605 m +1200 \
606 n -0100 \
607 o -0200 \
608 p -0300 \
609 q -0400 \
610 r -0500 \
611 s -0600 \
612 t -0700 \
613 u -0800 \
614 v -0900 \
615 w -1000 \
616 x -1100 \
617 y -1200 \
618 z +0000 \
621 # Caches
623 variable LocaleNumeralCache {}; # Dictionary whose keys are locale
624 # names and whose values are pairs
625 # comprising regexes matching numerals
626 # in the given locales and dictionaries
627 # mapping the numerals to their numeric
628 # values.
629 variable McLoaded {}; # Dictionary whose keys are locales
630 # in which [mcload] has been executed
631 # and whose values are second-level
632 # dictionaries indexed by message
633 # name and giving message text.
634 # variable CachedSystemTimeZone; # If 'CachedSystemTimeZone' exists,
635 # it contains the value of the
636 # system time zone, as determined from
637 # the environment.
638 variable TimeZoneBad {}; # Dictionary whose keys are time zone
639 # names and whose values are 1 if
640 # the time zone is unknown and 0
641 # if it is known.
642 variable TZData; # Array whose keys are time zone names
643 # and whose values are lists of quads
644 # comprising start time, UTC offset,
645 # Daylight Saving Time indicator, and
646 # time zone abbreviation.
647 variable FormatProc; # Array mapping format group
648 # and locale to the name of a procedure
649 # that renders the given format
651 ::tcl::clock::Initialize
653 #----------------------------------------------------------------------
655 # clock format --
657 # Formats a count of seconds since the Posix Epoch as a time
658 # of day.
660 # The 'clock format' command formats times of day for output.
661 # Refer to the user documentation to see what it does.
663 #----------------------------------------------------------------------
665 proc ::tcl::clock::format { args } {
667 variable FormatProc
668 variable TZData
670 lassign [ParseFormatArgs {*}$args] format locale timezone
671 set locale [string tolower $locale]
672 set clockval [lindex $args 0]
674 # Get the data for time changes in the given zone
676 if {$timezone eq ""} {
677 set timezone [GetSystemTimeZone]
679 if {![info exists TZData($timezone)]} {
680 if {[catch {SetupTimeZone $timezone} retval opts]} {
681 dict unset opts -errorinfo
682 return -options $opts $retval
686 # Build a procedure to format the result. Cache the built procedure's
687 # name in the 'FormatProc' array to avoid losing its internal
688 # representation, which contains the name resolution.
690 set procName formatproc'$format'$locale
691 set procName [namespace current]::[string map {: {\:} \\ {\\}} $procName]
692 if {[info exists FormatProc($procName)]} {
693 set procName $FormatProc($procName)
694 } else {
695 set FormatProc($procName) \
696 [ParseClockFormatFormat $procName $format $locale]
699 return [$procName $clockval $timezone]
703 #----------------------------------------------------------------------
705 # ParseClockFormatFormat --
707 # Builds and caches a procedure that formats a time value.
709 # Parameters:
710 # format -- Format string to use
711 # locale -- Locale in which the format string is to be interpreted
713 # Results:
714 # Returns the name of the newly-built procedure.
716 #----------------------------------------------------------------------
718 proc ::tcl::clock::ParseClockFormatFormat {procName format locale} {
720 if {[namespace which $procName] ne {}} {
721 return $procName
724 # Map away the locale-dependent composite format groups
726 EnterLocale $locale oldLocale
728 # Change locale if a fresh locale has been given on the command line.
730 set status [catch {
732 ParseClockFormatFormat2 $format $locale $procName
734 } result opts]
736 # Restore the locale
738 if { [info exists oldLocale] } {
739 mclocale $oldLocale
742 # Return either the error or the proc name
744 if { $status == 1 } {
745 if { [lindex [dict get $opts -errorcode] 0] eq {clock} } {
746 return -code error $result
747 } else {
748 return -options $opts $result
750 } else {
751 return $result
756 proc ::tcl::clock::ParseClockFormatFormat2 {format locale procName} {
758 set didLocaleEra 0
759 set didLocaleNumerals 0
760 set preFormatCode \
761 [string map [list @GREGORIAN_CHANGE_DATE@ \
762 [mc GREGORIAN_CHANGE_DATE]] \
764 variable TZData
765 set date [GetDateFields $clockval \
766 $TZData($timezone) \
767 @GREGORIAN_CHANGE_DATE@]
769 set formatString {}
770 set substituents {}
771 set state {}
773 set format [LocalizeFormat $locale $format]
775 foreach char [split $format {}] {
776 switch -exact -- $state {
777 {} {
778 if { [string equal % $char] } {
779 set state percent
780 } else {
781 append formatString $char
784 percent { # Character following a '%' character
785 set state {}
786 switch -exact -- $char {
787 % { # A literal character, '%'
788 append formatString %%
790 a { # Day of week, abbreviated
791 append formatString %s
792 append substituents \
793 [string map \
794 [list @DAYS_OF_WEEK_ABBREV@ \
795 [list [mc DAYS_OF_WEEK_ABBREV]]] \
796 { [lindex @DAYS_OF_WEEK_ABBREV@ \
797 [expr {[dict get $date dayOfWeek] \
798 % 7}]]}]
800 A { # Day of week, spelt out.
801 append formatString %s
802 append substituents \
803 [string map \
804 [list @DAYS_OF_WEEK_FULL@ \
805 [list [mc DAYS_OF_WEEK_FULL]]] \
806 { [lindex @DAYS_OF_WEEK_FULL@ \
807 [expr {[dict get $date dayOfWeek] \
808 % 7}]]}]
810 b - h { # Name of month, abbreviated.
811 append formatString %s
812 append substituents \
813 [string map \
814 [list @MONTHS_ABBREV@ \
815 [list [mc MONTHS_ABBREV]]] \
816 { [lindex @MONTHS_ABBREV@ \
817 [expr {[dict get $date month]-1}]]}]
819 B { # Name of month, spelt out
820 append formatString %s
821 append substituents \
822 [string map \
823 [list @MONTHS_FULL@ \
824 [list [mc MONTHS_FULL]]] \
825 { [lindex @MONTHS_FULL@ \
826 [expr {[dict get $date month]-1}]]}]
828 C { # Century number
829 append formatString %02d
830 append substituents \
831 { [expr {[dict get $date year] / 100}]}
833 d { # Day of month, with leading zero
834 append formatString %02d
835 append substituents { [dict get $date dayOfMonth]}
837 e { # Day of month, without leading zero
838 append formatString %2d
839 append substituents { [dict get $date dayOfMonth]}
841 E { # Format group in a locale-dependent
842 # alternative era
843 set state percentE
844 if {!$didLocaleEra} {
845 append preFormatCode \
846 [string map \
847 [list @LOCALE_ERAS@ \
848 [list [mc LOCALE_ERAS]]] \
850 set date [GetLocaleEra \
851 $date[set date {}] \
852 @LOCALE_ERAS@]}] \n
853 set didLocaleEra 1
855 if {!$didLocaleNumerals} {
856 append preFormatCode \
857 [list set localeNumerals \
858 [mc LOCALE_NUMERALS]] \n
859 set didLocaleNumerals 1
862 g { # Two-digit year relative to ISO8601
863 # week number
864 append formatString %02d
865 append substituents \
866 { [expr { [dict get $date iso8601Year] % 100 }]}
868 G { # Four-digit year relative to ISO8601
869 # week number
870 append formatString %02d
871 append substituents { [dict get $date iso8601Year]}
873 H { # Hour in the 24-hour day, leading zero
874 append formatString %02d
875 append substituents \
876 { [expr { [dict get $date localSeconds] \
877 / 3600 % 24}]}
879 I { # Hour AM/PM, with leading zero
880 append formatString %02d
881 append substituents \
882 { [expr { ( ( ( [dict get $date localSeconds] \
883 % 86400 ) \
884 + 86400 \
885 - 3600 ) \
886 / 3600 ) \
887 % 12 + 1 }] }
889 j { # Day of year (001-366)
890 append formatString %03d
891 append substituents { [dict get $date dayOfYear]}
893 J { # Julian Day Number
894 append formatString %07ld
895 append substituents { [dict get $date julianDay]}
897 k { # Hour (0-23), no leading zero
898 append formatString %2d
899 append substituents \
900 { [expr { [dict get $date localSeconds]
901 / 3600
902 % 24 }]}
904 l { # Hour (12-11), no leading zero
905 append formatString %2d
906 append substituents \
907 { [expr { ( ( ( [dict get $date localSeconds]
908 % 86400 )
909 + 86400
910 - 3600 )
911 / 3600 )
912 % 12 + 1 }]}
914 m { # Month number, leading zero
915 append formatString %02d
916 append substituents { [dict get $date month]}
918 M { # Minute of the hour, leading zero
919 append formatString %02d
920 append substituents \
921 { [expr { [dict get $date localSeconds]
922 / 60
923 % 60 }]}
925 n { # A literal newline
926 append formatString \n
928 N { # Month number, no leading zero
929 append formatString %2d
930 append substituents { [dict get $date month]}
932 O { # A format group in the locale's
933 # alternative numerals
934 set state percentO
935 if {!$didLocaleNumerals} {
936 append preFormatCode \
937 [list set localeNumerals \
938 [mc LOCALE_NUMERALS]] \n
939 set didLocaleNumerals 1
942 p { # Localized 'AM' or 'PM' indicator
943 # converted to uppercase
944 append formatString %s
945 append preFormatCode \
946 [list set AM [string toupper [mc AM]]] \n \
947 [list set PM [string toupper [mc PM]]] \n
948 append substituents \
949 { [expr {(([dict get $date localSeconds]
950 % 86400) < 43200) ?
951 $AM : $PM}]}
953 P { # Localized 'AM' or 'PM' indicator
954 append formatString %s
955 append preFormatCode \
956 [list set am [mc AM]] \n \
957 [list set pm [mc PM]] \n
958 append substituents \
959 { [expr {(([dict get $date localSeconds]
960 % 86400) < 43200) ?
961 $am : $pm}]}
964 Q { # Hi, Jeff!
965 append formatString %s
966 append substituents { [FormatStarDate $date]}
968 s { # Seconds from the Posix Epoch
969 append formatString %s
970 append substituents { [dict get $date seconds]}
972 S { # Second of the minute, with
973 # leading zero
974 append formatString %02d
975 append substituents \
976 { [expr { [dict get $date localSeconds]
977 % 60 }]}
979 t { # A literal tab character
980 append formatString \t
982 u { # Day of the week (1-Monday, 7-Sunday)
983 append formatString %1d
984 append substituents { [dict get $date dayOfWeek]}
986 U { # Week of the year (00-53). The
987 # first Sunday of the year is the
988 # first day of week 01
989 append formatString %02d
990 append preFormatCode {
991 set dow [dict get $date dayOfWeek]
992 if { $dow == 7 } {
993 set dow 0
995 incr dow
996 set UweekNumber \
997 [expr { ( [dict get $date dayOfYear]
998 - $dow + 7 )
999 / 7 }]
1001 append substituents { $UweekNumber}
1003 V { # The ISO8601 week number
1004 append formatString %02d
1005 append substituents { [dict get $date iso8601Week]}
1007 w { # Day of the week (0-Sunday,
1008 # 6-Saturday)
1009 append formatString %1d
1010 append substituents \
1011 { [expr { [dict get $date dayOfWeek] % 7 }]}
1013 W { # Week of the year (00-53). The first
1014 # Monday of the year is the first day
1015 # of week 01.
1016 append preFormatCode {
1017 set WweekNumber \
1018 [expr { ( [dict get $date dayOfYear]
1019 - [dict get $date dayOfWeek]
1020 + 7 )
1021 / 7 }]
1023 append formatString %02d
1024 append substituents { $WweekNumber}
1026 y { # The two-digit year of the century
1027 append formatString %02d
1028 append substituents \
1029 { [expr { [dict get $date year] % 100 }]}
1031 Y { # The four-digit year
1032 append formatString %04d
1033 append substituents { [dict get $date year]}
1035 z { # The time zone as hours and minutes
1036 # east (+) or west (-) of Greenwich
1037 append formatString %s
1038 append substituents { [FormatNumericTimeZone \
1039 [dict get $date tzOffset]]}
1041 Z { # The name of the time zone
1042 append formatString %s
1043 append substituents { [dict get $date tzName]}
1045 % { # A literal percent character
1046 append formatString %%
1048 default { # An unknown escape sequence
1049 append formatString %% $char
1053 percentE { # Character following %E
1054 set state {}
1055 switch -exact -- $char {
1057 append formatString %s
1058 append substituents { } \
1059 [string map \
1060 [list @BCE@ [list [mc BCE]] \
1061 @CE@ [list [mc CE]]] \
1062 {[dict get {BCE @BCE@ CE @CE@} \
1063 [dict get $date era]]}]
1065 C { # Locale-dependent era
1066 append formatString %s
1067 append substituents { [dict get $date localeEra]}
1069 y { # Locale-dependent year of the era
1070 append preFormatCode {
1071 set y [dict get $date localeYear]
1072 if { $y >= 0 && $y < 100 } {
1073 set Eyear [lindex $localeNumerals $y]
1074 } else {
1075 set Eyear $y
1078 append formatString %s
1079 append substituents { $Eyear}
1081 default { # Unknown %E format group
1082 append formatString %%E $char
1086 percentO { # Character following %O
1087 set state {}
1088 switch -exact -- $char {
1089 d - e { # Day of the month in alternative
1090 # numerals
1091 append formatString %s
1092 append substituents \
1093 { [lindex $localeNumerals \
1094 [dict get $date dayOfMonth]]}
1096 H - k { # Hour of the day in alternative
1097 # numerals
1098 append formatString %s
1099 append substituents \
1100 { [lindex $localeNumerals \
1101 [expr { [dict get $date localSeconds]
1102 / 3600
1103 % 24 }]]}
1105 I - l { # Hour (12-11) AM/PM in alternative
1106 # numerals
1107 append formatString %s
1108 append substituents \
1109 { [lindex $localeNumerals \
1110 [expr { ( ( ( [dict get $date localSeconds]
1111 % 86400 )
1112 + 86400
1113 - 3600 )
1114 / 3600 )
1115 % 12 + 1 }]]}
1117 m { # Month number in alternative numerals
1118 append formatString %s
1119 append substituents \
1120 { [lindex $localeNumerals [dict get $date month]]}
1122 M { # Minute of the hour in alternative
1123 # numerals
1124 append formatString %s
1125 append substituents \
1126 { [lindex $localeNumerals \
1127 [expr { [dict get $date localSeconds]
1128 / 60
1129 % 60 }]]}
1131 S { # Second of the minute in alternative
1132 # numerals
1133 append formatString %s
1134 append substituents \
1135 { [lindex $localeNumerals \
1136 [expr { [dict get $date localSeconds]
1137 % 60 }]]}
1139 u { # Day of the week (Monday=1,Sunday=7)
1140 # in alternative numerals
1141 append formatString %s
1142 append substituents \
1143 { [lindex $localeNumerals \
1144 [dict get $date dayOfWeek]]}
1146 w { # Day of the week (Sunday=0,Saturday=6)
1147 # in alternative numerals
1148 append formatString %s
1149 append substituents \
1150 { [lindex $localeNumerals \
1151 [expr { [dict get $date dayOfWeek] % 7 }]]}
1153 y { # Year of the century in alternative
1154 # numerals
1155 append formatString %s
1156 append substituents \
1157 { [lindex $localeNumerals \
1158 [expr { [dict get $date year] % 100 }]]}
1160 default { # Unknown format group
1161 append formatString %%O $char
1168 # Clean up any improperly terminated groups
1170 switch -exact -- $state {
1171 percent {
1172 append formatString %%
1174 percentE {
1175 append retval %%E
1177 percentO {
1178 append retval %%O
1182 proc $procName {clockval timezone} "
1183 $preFormatCode
1184 return \[::format [list $formatString] $substituents\]
1187 # puts [list $procName [info args $procName] [info body $procName]]
1189 return $procName
1192 #----------------------------------------------------------------------
1194 # clock scan --
1196 # Inputs a count of seconds since the Posix Epoch as a time
1197 # of day.
1199 # The 'clock format' command scans times of day on input.
1200 # Refer to the user documentation to see what it does.
1202 #----------------------------------------------------------------------
1204 proc ::tcl::clock::scan { args } {
1206 set format {}
1208 # Check the count of args
1210 if { [llength $args] < 1 || [llength $args] % 2 != 1 } {
1211 set cmdName "clock scan"
1212 return -code error \
1213 -errorcode [list CLOCK wrongNumArgs] \
1214 "wrong \# args: should be\
1215 \"$cmdName string\
1216 ?-base seconds?\
1217 ?-format string? ?-gmt boolean?\
1218 ?-locale LOCALE? ?-timezone ZONE?\""
1221 # Set defaults
1223 set base [clock seconds]
1224 set string [lindex $args 0]
1225 set format {}
1226 set gmt 0
1227 set locale c
1228 set timezone [GetSystemTimeZone]
1230 # Pick up command line options.
1232 foreach { flag value } [lreplace $args 0 0] {
1233 set saw($flag) {}
1234 switch -exact -- $flag {
1235 -b - -ba - -bas - -base {
1236 set base $value
1238 -f - -fo - -for - -form - -forma - -format {
1239 set format $value
1241 -g - -gm - -gmt {
1242 set gmt $value
1244 -l - -lo - -loc - -loca - -local - -locale {
1245 set locale [string tolower $value]
1247 -t - -ti - -tim - -time - -timez - -timezo - -timezon - -timezone {
1248 set timezone $value
1250 default {
1251 return -code error \
1252 -errorcode [list CLOCK badSwitch $flag] \
1253 "bad switch \"$flag\",\
1254 must be -base, -format, -gmt, -locale or -timezone"
1259 # Check options for validity
1261 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
1262 return -code error \
1263 -errorcode [list CLOCK gmtWithTimezone] \
1264 "cannot use -gmt and -timezone in same call"
1266 if { [catch { expr { wide($base) } } result] } {
1267 return -code error \
1268 "expected integer but got \"$base\""
1270 if { ![string is boolean $gmt] } {
1271 return -code error \
1272 "expected boolean value but got \"$gmt\""
1273 } else {
1274 if { $gmt } {
1275 set timezone :GMT
1279 if { ![info exists saw(-format)] } {
1280 # Perhaps someday we'll localize the legacy code. Right now,
1281 # it's not localized.
1282 if { [info exists saw(-locale)] } {
1283 return -code error \
1284 -errorcode [list CLOCK flagWithLegacyFormat] \
1285 "legacy \[clock scan\] does not support -locale"
1288 return [FreeScan $string $base $timezone $locale]
1291 # Change locale if a fresh locale has been given on the command line.
1293 EnterLocale $locale oldLocale
1295 set status [catch {
1297 # Map away the locale-dependent composite format groups
1299 set scanner [ParseClockScanFormat $format $locale]
1300 $scanner $string $base $timezone
1302 } result opts]
1304 # Restore the locale
1306 if { [info exists oldLocale] } {
1307 mclocale $oldLocale
1310 if { $status == 1 } {
1311 if { [lindex [dict get $opts -errorcode] 0] eq {clock} } {
1312 return -code error $result
1313 } else {
1314 return -options $opts $result
1316 } else {
1317 return $result
1322 #----------------------------------------------------------------------
1324 # FreeScan --
1326 # Scans a time in free format
1328 # Parameters:
1329 # string - String containing the time to scan
1330 # base - Base time, expressed in seconds from the Epoch
1331 # timezone - Default time zone in which the time will be expressed
1332 # locale - (Unused) Name of the locale where the time will be scanned.
1334 # Results:
1335 # Returns the date and time extracted from the string in seconds
1336 # from the epoch
1338 #----------------------------------------------------------------------
1340 proc ::tcl::clock::FreeScan { string base timezone locale } {
1342 variable TZData
1344 # Get the data for time changes in the given zone
1346 if {[catch {SetupTimeZone $timezone} retval opts]} {
1347 dict unset opts -errorinfo
1348 return -options $opts $retval
1351 # Extract year, month and day from the base time for the
1352 # parser to use as defaults
1354 set date [GetDateFields \
1355 $base \
1356 $TZData($timezone) \
1357 2361222]
1358 dict set date secondOfDay [expr { [dict get $date localSeconds]
1359 % 86400 }]
1361 # Parse the date. The parser will return a list comprising
1362 # date, time, time zone, relative month/day/seconds, relative
1363 # weekday, ordinal month.
1365 set status [catch {
1366 Oldscan $string \
1367 [dict get $date year] \
1368 [dict get $date month] \
1369 [dict get $date dayOfMonth]
1370 } result]
1371 if { $status != 0 } {
1372 return -code error "unable to convert date-time string \"$string\": $result"
1375 lassign $result parseDate parseTime parseZone parseRel \
1376 parseWeekday parseOrdinalMonth
1378 # If the caller supplied a date in the string, update the 'date' dict
1379 # with the value. If the caller didn't specify a time with the date,
1380 # default to midnight.
1382 if { [llength $parseDate] > 0 } {
1383 lassign $parseDate y m d
1384 if { $y < 100 } {
1385 if { $y >= 39 } {
1386 incr y 1900
1387 } else {
1388 incr y 2000
1391 dict set date era CE
1392 dict set date year $y
1393 dict set date month $m
1394 dict set date dayOfMonth $d
1395 if { $parseTime eq {} } {
1396 set parseTime 0
1400 # If the caller supplied a time zone in the string, it comes back
1401 # as a two-element list; the first element is the number of minutes
1402 # east of Greenwich, and the second is a Daylight Saving Time
1403 # indicator ( 1 == yes, 0 == no, -1 == unknown ). We make it into
1404 # a time zone indicator of +-hhmm.
1406 if { [llength $parseZone] > 0 } {
1407 lassign $parseZone minEast dstFlag
1408 set timezone [FormatNumericTimeZone \
1409 [expr { 60 * $minEast + 3600 * $dstFlag }]]
1410 SetupTimeZone $timezone
1412 dict set date tzName $timezone
1414 # Assemble date, time, zone into seconds-from-epoch
1416 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] 2361222]
1417 if { $parseTime ne {} } {
1418 dict set date secondOfDay $parseTime
1419 } elseif { [llength $parseWeekday] != 0
1420 || [llength $parseOrdinalMonth] != 0
1421 || ( [llength $parseRel] != 0
1422 && ( [lindex $parseRel 0] != 0
1423 || [lindex $parseRel 1] != 0 ) ) } {
1424 dict set date secondOfDay 0
1427 dict set date localSeconds \
1428 [expr { -210866803200
1429 + ( 86400 * wide([dict get $date julianDay]) )
1430 + [dict get $date secondOfDay] }]
1431 dict set date tzName $timezone
1432 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) 2361222]
1433 set seconds [dict get $date seconds]
1435 # Do relative times
1437 if { [llength $parseRel] > 0 } {
1438 lassign $parseRel relMonth relDay relSecond
1439 set seconds [add $seconds \
1440 $relMonth months $relDay days $relSecond seconds \
1441 -timezone $timezone -locale $locale]
1444 # Do relative weekday
1446 if { [llength $parseWeekday] > 0 } {
1448 lassign $parseWeekday dayOrdinal dayOfWeek
1449 set date2 [GetDateFields $seconds $TZData($timezone) 2361222]
1450 dict set date2 era CE
1451 set jdwkday [WeekdayOnOrBefore $dayOfWeek \
1452 [expr { [dict get $date2 julianDay]
1453 + 6 }]]
1454 incr jdwkday [expr { 7 * $dayOrdinal }]
1455 if { $dayOrdinal > 0 } {
1456 incr jdwkday -7
1458 dict set date2 secondOfDay \
1459 [expr { [dict get $date2 localSeconds] % 86400 }]
1460 dict set date2 julianDay $jdwkday
1461 dict set date2 localSeconds \
1462 [expr { -210866803200
1463 + ( 86400 * wide([dict get $date2 julianDay]) )
1464 + [dict get $date secondOfDay] }]
1465 dict set date2 tzName $timezone
1466 set date2 [ConvertLocalToUTC $date2[set date2 {}] $TZData($timezone) \
1467 2361222]
1468 set seconds [dict get $date2 seconds]
1472 # Do relative month
1474 if { [llength $parseOrdinalMonth] > 0 } {
1476 lassign $parseOrdinalMonth monthOrdinal monthNumber
1477 if { $monthOrdinal > 0 } {
1478 set monthDiff [expr { $monthNumber - [dict get $date month] }]
1479 if { $monthDiff <= 0 } {
1480 incr monthDiff 12
1482 incr monthOrdinal -1
1483 } else {
1484 set monthDiff [expr { [dict get $date month] - $monthNumber }]
1485 if { $monthDiff >= 0 } {
1486 incr monthDiff -12
1488 incr monthOrdinal
1490 set seconds [add $seconds $monthOrdinal years $monthDiff months \
1491 -timezone $timezone -locale $locale]
1495 return $seconds
1499 #----------------------------------------------------------------------
1501 # ParseClockScanFormat --
1503 # Parses a format string given to [clock scan -format]
1505 # Parameters:
1506 # formatString - The format being parsed
1507 # locale - The current locale
1509 # Results:
1510 # Constructs and returns a procedure that accepts the
1511 # string being scanned, the base time, and the time zone.
1512 # The procedure will either return the scanned time or
1513 # else throw an error that should be rethrown to the caller
1514 # of [clock scan]
1516 # Side effects:
1517 # The given procedure is defined in the ::tcl::clock
1518 # namespace. Scan procedures are not deleted once installed.
1520 # Why do we parse dates by defining a procedure to parse them?
1521 # The reason is that by doing so, we have one convenient place to
1522 # cache all the information: the regular expressions that match the
1523 # patterns (which will be compiled), the code that assembles the
1524 # date information, everything lands in one place. In this way,
1525 # when a given format is reused at run time, all the information
1526 # of how to apply it is available in a single place.
1528 #----------------------------------------------------------------------
1530 proc ::tcl::clock::ParseClockScanFormat {formatString locale} {
1532 # Check whether the format has been parsed previously, and return
1533 # the existing recognizer if it has.
1535 set procName scanproc'$formatString'$locale
1536 set procName [namespace current]::[string map {: {\:} \\ {\\}} $procName]
1537 if { [namespace which $procName] != {} } {
1538 return $procName
1541 variable DateParseActions
1542 variable TimeParseActions
1544 # Localize the %x, %X, etc. groups
1546 set formatString [LocalizeFormat $locale $formatString]
1548 # Condense whitespace
1550 regsub -all {[[:space:]]+} $formatString { } formatString
1552 # Walk through the groups of the format string. In this loop, we
1553 # accumulate:
1554 # - a regular expression that matches the string,
1555 # - the count of capturing brackets in the regexp
1556 # - a set of code that post-processes the fields captured by the regexp,
1557 # - a dictionary whose keys are the names of fields that are present
1558 # in the format string.
1560 set re {^[[:space:]]*}
1561 set captureCount 0
1562 set postcode {}
1563 set fieldSet [dict create]
1564 set fieldCount 0
1565 set postSep {}
1566 set state {}
1568 foreach c [split $formatString {}] {
1569 switch -exact -- $state {
1570 {} {
1571 if { $c eq "%" } {
1572 set state %
1573 } elseif { $c eq " " } {
1574 append re {[[:space:]]+}
1575 } else {
1576 if { ! [string is alnum $c] } {
1577 append re \\
1579 append re $c
1583 set state {}
1584 switch -exact -- $c {
1586 append re %
1588 { } {
1589 append re "\[\[:space:\]\]*"
1591 a - A { # Day of week, in words
1592 set l {}
1593 foreach \
1594 i {7 1 2 3 4 5 6} \
1595 abr [mc DAYS_OF_WEEK_ABBREV] \
1596 full [mc DAYS_OF_WEEK_FULL] {
1597 dict set l [string tolower $abr] $i
1598 dict set l [string tolower $full] $i
1599 incr i
1601 lassign [UniquePrefixRegexp $l] regex lookup
1602 append re ( $regex )
1603 dict set fieldSet dayOfWeek [incr fieldCount]
1604 append postcode "dict set date dayOfWeek \[" \
1605 "dict get " [list $lookup] " " \
1606 \[ {string tolower $field} [incr captureCount] \] \
1607 "\]\n"
1609 b - B - h { # Name of month
1610 set i 0
1611 set l {}
1612 foreach \
1613 abr [mc MONTHS_ABBREV] \
1614 full [mc MONTHS_FULL] {
1615 incr i
1616 dict set l [string tolower $abr] $i
1617 dict set l [string tolower $full] $i
1619 lassign [UniquePrefixRegexp $l] regex lookup
1620 append re ( $regex )
1621 dict set fieldSet month [incr fieldCount]
1622 append postcode "dict set date month \[" \
1623 "dict get " [list $lookup] \
1624 " " \[ {string tolower $field} \
1625 [incr captureCount] \] \
1626 "\]\n"
1628 C { # Gregorian century
1629 append re \\s*(\\d\\d?)
1630 dict set fieldSet century [incr fieldCount]
1631 append postcode "dict set date century \[" \
1632 "::scan \$field" [incr captureCount] " %d" \
1633 "\]\n"
1635 d - e { # Day of month
1636 append re \\s*(\\d\\d?)
1637 dict set fieldSet dayOfMonth [incr fieldCount]
1638 append postcode "dict set date dayOfMonth \[" \
1639 "::scan \$field" [incr captureCount] " %d" \
1640 "\]\n"
1642 E { # Prefix for locale-specific codes
1643 set state %E
1645 g { # ISO8601 2-digit year
1646 append re \\s*(\\d\\d)
1647 dict set fieldSet iso8601YearOfCentury \
1648 [incr fieldCount]
1649 append postcode \
1650 "dict set date iso8601YearOfCentury \[" \
1651 "::scan \$field" [incr captureCount] " %d" \
1652 "\]\n"
1654 G { # ISO8601 4-digit year
1655 append re \\s*(\\d\\d)(\\d\\d)
1656 dict set fieldSet iso8601Century [incr fieldCount]
1657 dict set fieldSet iso8601YearOfCentury \
1658 [incr fieldCount]
1659 append postcode \
1660 "dict set date iso8601Century \[" \
1661 "::scan \$field" [incr captureCount] " %d" \
1662 "\]\n" \
1663 "dict set date iso8601YearOfCentury \[" \
1664 "::scan \$field" [incr captureCount] " %d" \
1665 "\]\n"
1667 H - k { # Hour of day
1668 append re \\s*(\\d\\d?)
1669 dict set fieldSet hour [incr fieldCount]
1670 append postcode "dict set date hour \[" \
1671 "::scan \$field" [incr captureCount] " %d" \
1672 "\]\n"
1674 I - l { # Hour, AM/PM
1675 append re \\s*(\\d\\d?)
1676 dict set fieldSet hourAMPM [incr fieldCount]
1677 append postcode "dict set date hourAMPM \[" \
1678 "::scan \$field" [incr captureCount] " %d" \
1679 "\]\n"
1681 j { # Day of year
1682 append re \\s*(\\d\\d?\\d?)
1683 dict set fieldSet dayOfYear [incr fieldCount]
1684 append postcode "dict set date dayOfYear \[" \
1685 "::scan \$field" [incr captureCount] " %d" \
1686 "\]\n"
1688 J { # Julian Day Number
1689 append re \\s*(\\d+)
1690 dict set fieldSet julianDay [incr fieldCount]
1691 append postcode "dict set date julianDay \[" \
1692 "::scan \$field" [incr captureCount] " %ld" \
1693 "\]\n"
1695 m - N { # Month number
1696 append re \\s*(\\d\\d?)
1697 dict set fieldSet month [incr fieldCount]
1698 append postcode "dict set date month \[" \
1699 "::scan \$field" [incr captureCount] " %d" \
1700 "\]\n"
1702 M { # Minute
1703 append re \\s*(\\d\\d?)
1704 dict set fieldSet minute [incr fieldCount]
1705 append postcode "dict set date minute \[" \
1706 "::scan \$field" [incr captureCount] " %d" \
1707 "\]\n"
1709 n { # Literal newline
1710 append re \\n
1712 O { # Prefix for locale numerics
1713 set state %O
1715 p - P { # AM/PM indicator
1716 set l [list [string tolower [mc AM]] 0 \
1717 [string tolower [mc PM]] 1]
1718 lassign [UniquePrefixRegexp $l] regex lookup
1719 append re ( $regex )
1720 dict set fieldSet amPmIndicator [incr fieldCount]
1721 append postcode "dict set date amPmIndicator \[" \
1722 "dict get " [list $lookup] " \[string tolower " \
1723 "\$field" \
1724 [incr captureCount] \
1725 "\]\]\n"
1727 Q { # Hi, Jeff!
1728 append re {Stardate\s+([-+]?\d+)(\d\d\d)[.](\d)}
1729 incr captureCount
1730 dict set fieldSet seconds [incr fieldCount]
1731 append postcode {dict set date seconds } \[ \
1732 {ParseStarDate $field} [incr captureCount] \
1733 { $field} [incr captureCount] \
1734 { $field} [incr captureCount] \
1735 \] \n
1737 s { # Seconds from Posix Epoch
1738 # This next case is insanely difficult,
1739 # because it's problematic to determine
1740 # whether the field is actually within
1741 # the range of a wide integer.
1742 append re {\s*([-+]?\d+)}
1743 dict set fieldSet seconds [incr fieldCount]
1744 append postcode {dict set date seconds } \[ \
1745 {ScanWide $field} [incr captureCount] \] \n
1747 S { # Second
1748 append re \\s*(\\d\\d?)
1749 dict set fieldSet second [incr fieldCount]
1750 append postcode "dict set date second \[" \
1751 "::scan \$field" [incr captureCount] " %d" \
1752 "\]\n"
1754 t { # Literal tab character
1755 append re \\t
1757 u - w { # Day number within week, 0 or 7 == Sun
1758 # 1=Mon, 6=Sat
1759 append re \\s*(\\d)
1760 dict set fieldSet dayOfWeek [incr fieldCount]
1761 append postcode {::scan $field} [incr captureCount] \
1762 { %d dow} \n \
1764 if { $dow == 0 } {
1765 set dow 7
1766 } elseif { $dow > 7 } {
1767 return -code error \
1768 -errorcode [list CLOCK badDayOfWeek] \
1769 "day of week is greater than 7"
1771 dict set date dayOfWeek $dow
1774 U { # Week of year. The
1775 # first Sunday of the year is the
1776 # first day of week 01. No scan rule
1777 # uses this group.
1778 append re \\s*\\d\\d?
1780 V { # Week of ISO8601 year
1782 append re \\s*(\\d\\d?)
1783 dict set fieldSet iso8601Week [incr fieldCount]
1784 append postcode "dict set date iso8601Week \[" \
1785 "::scan \$field" [incr captureCount] " %d" \
1786 "\]\n"
1788 W { # Week of the year (00-53). The first
1789 # Monday of the year is the first day
1790 # of week 01. No scan rule uses this
1791 # group.
1792 append re \\s*\\d\\d?
1794 y { # Two-digit Gregorian year
1795 append re \\s*(\\d\\d?)
1796 dict set fieldSet yearOfCentury [incr fieldCount]
1797 append postcode "dict set date yearOfCentury \[" \
1798 "::scan \$field" [incr captureCount] " %d" \
1799 "\]\n"
1801 Y { # 4-digit Gregorian year
1802 append re \\s*(\\d\\d)(\\d\\d)
1803 dict set fieldSet century [incr fieldCount]
1804 dict set fieldSet yearOfCentury [incr fieldCount]
1805 append postcode \
1806 "dict set date century \[" \
1807 "::scan \$field" [incr captureCount] " %d" \
1808 "\]\n" \
1809 "dict set date yearOfCentury \[" \
1810 "::scan \$field" [incr captureCount] " %d" \
1811 "\]\n"
1813 z - Z { # Time zone name
1814 append re {(?:([-+]\d\d(?::?\d\d(?::?\d\d)?)?)|([[:alnum:]]{1,4}))}
1815 dict set fieldSet tzName [incr fieldCount]
1816 append postcode \
1817 {if } \{ { $field} [incr captureCount] \
1818 { ne "" } \} { } \{ \n \
1819 {dict set date tzName $field} \
1820 $captureCount \n \
1821 \} { else } \{ \n \
1822 {dict set date tzName } \[ \
1823 {ConvertLegacyTimeZone $field} \
1824 [incr captureCount] \] \n \
1825 \} \n \
1827 % { # Literal percent character
1828 append re %
1830 default {
1831 append re %
1832 if { ! [string is alnum $c] } {
1833 append re \\
1835 append re $c
1839 %E {
1840 switch -exact -- $c {
1841 C { # Locale-dependent era
1842 set d {}
1843 foreach triple [mc LOCALE_ERAS] {
1844 lassign $triple t symbol year
1845 dict set d [string tolower $symbol] $year
1847 lassign [UniquePrefixRegexp $d] regex lookup
1848 append re (?: $regex )
1851 set l {}
1852 dict set l [string tolower [mc BCE]] BCE
1853 dict set l [string tolower [mc CE]] CE
1854 dict set l b.c.e. BCE
1855 dict set l c.e. CE
1856 dict set l b.c. BCE
1857 dict set l a.d. CE
1858 lassign [UniquePrefixRegexp $l] regex lookup
1859 append re ( $regex )
1860 dict set fieldSet era [incr fieldCount]
1861 append postcode "dict set date era \["\
1862 "dict get " [list $lookup] \
1863 { } \[ {string tolower $field} \
1864 [incr captureCount] \] \
1865 "\]\n"
1867 y { # Locale-dependent year of the era
1868 lassign [LocaleNumeralMatcher $locale] regex lookup
1869 append re $regex
1870 incr captureCount
1872 default {
1873 append re %E
1874 if { ! [string is alnum $c] } {
1875 append re \\
1877 append re $c
1880 set state {}
1882 %O {
1883 switch -exact -- $c {
1884 d - e {
1885 lassign [LocaleNumeralMatcher $locale] regex lookup
1886 append re $regex
1887 dict set fieldSet dayOfMonth [incr fieldCount]
1888 append postcode "dict set date dayOfMonth \[" \
1889 "dict get " [list $lookup] " \$field" \
1890 [incr captureCount] \
1891 "\]\n"
1893 H - k {
1894 lassign [LocaleNumeralMatcher $locale] regex lookup
1895 append re $regex
1896 dict set fieldSet hour [incr fieldCount]
1897 append postcode "dict set date hour \[" \
1898 "dict get " [list $lookup] " \$field" \
1899 [incr captureCount] \
1900 "\]\n"
1902 I - l {
1903 lassign [LocaleNumeralMatcher $locale] regex lookup
1904 append re $regex
1905 dict set fieldSet hourAMPM [incr fieldCount]
1906 append postcode "dict set date hourAMPM \[" \
1907 "dict get " [list $lookup] " \$field" \
1908 [incr captureCount] \
1909 "\]\n"
1912 lassign [LocaleNumeralMatcher $locale] regex lookup
1913 append re $regex
1914 dict set fieldSet month [incr fieldCount]
1915 append postcode "dict set date month \[" \
1916 "dict get " [list $lookup] " \$field" \
1917 [incr captureCount] \
1918 "\]\n"
1921 lassign [LocaleNumeralMatcher $locale] regex lookup
1922 append re $regex
1923 dict set fieldSet minute [incr fieldCount]
1924 append postcode "dict set date minute \[" \
1925 "dict get " [list $lookup] " \$field" \
1926 [incr captureCount] \
1927 "\]\n"
1930 lassign [LocaleNumeralMatcher $locale] regex lookup
1931 append re $regex
1932 dict set fieldSet second [incr fieldCount]
1933 append postcode "dict set date second \[" \
1934 "dict get " [list $lookup] " \$field" \
1935 [incr captureCount] \
1936 "\]\n"
1938 u - w {
1939 lassign [LocaleNumeralMatcher $locale] regex lookup
1940 append re $regex
1941 dict set fieldSet dayOfWeek [incr fieldCount]
1942 append postcode "set dow \[dict get " [list $lookup] \
1943 { $field} [incr captureCount] \] \n \
1945 if { $dow == 0 } {
1946 set dow 7
1947 } elseif { $dow > 7 } {
1948 return -code error \
1949 -errorcode [list CLOCK badDayOfWeek] \
1950 "day of week is greater than 7"
1952 dict set date dayOfWeek $dow
1956 lassign [LocaleNumeralMatcher $locale] regex lookup
1957 append re $regex
1958 dict set fieldSet yearOfCentury [incr fieldCount]
1959 append postcode {dict set date yearOfCentury } \[ \
1960 {dict get } [list $lookup] { $field} \
1961 [incr captureCount] \] \n
1963 default {
1964 append re %O
1965 if { ! [string is alnum $c] } {
1966 append re \\
1968 append re $c
1971 set state {}
1976 # Clean up any unfinished format groups
1978 append re $state \\s*\$
1980 # Build the procedure
1982 set procBody {}
1983 append procBody "variable ::tcl::clock::TZData" \n
1984 append procBody "if \{ !\[ regexp -nocase [list $re] \$string ->"
1985 for { set i 1 } { $i <= $captureCount } { incr i } {
1986 append procBody " " field $i
1988 append procBody "\] \} \{" \n
1989 append procBody {
1990 return -code error -errorcode [list CLOCK badInputString] \
1991 {input string does not match supplied format}
1993 append procBody \}\n
1994 append procBody "set date \[dict create\]" \n
1995 append procBody {dict set date tzName $timeZone} \n
1996 append procBody $postcode
1997 append procBody [list set changeover [mc GREGORIAN_CHANGE_DATE]] \n
1999 # Get time zone if needed
2001 if { ![dict exists $fieldSet seconds]
2002 && ![dict exists $fieldSet starDate] } {
2003 if { [dict exists $fieldSet tzName] } {
2004 append procBody {
2005 set timeZone [dict get $date tzName]
2008 append procBody {
2009 ::tcl::clock::SetupTimeZone $timeZone
2013 # Add code that gets Julian Day Number from the fields.
2015 append procBody [MakeParseCodeFromFields $fieldSet $DateParseActions]
2017 # Get time of day
2019 append procBody [MakeParseCodeFromFields $fieldSet $TimeParseActions]
2021 # Assemble seconds, and convert local nominal time to UTC.
2023 if { ![dict exists $fieldSet seconds]
2024 && ![dict exists $fieldSet starDate] } {
2025 append procBody {
2026 if { [dict get $date julianDay] > 5373484 } {
2027 return -code error -errorcode [list CLOCK dateTooLarge] \
2028 "requested date too large to represent"
2030 dict set date localSeconds \
2031 [expr { -210866803200
2032 + ( 86400 * wide([dict get $date julianDay]) )
2033 + [dict get $date secondOfDay] }]
2035 append procBody {
2036 set date [::tcl::clock::ConvertLocalToUTC $date[set date {}] \
2037 $TZData($timeZone) \
2038 $changeover]
2042 # Return result
2044 append procBody {return [dict get $date seconds]} \n
2046 proc $procName { string baseTime timeZone } $procBody
2048 # puts [list proc $procName [list string baseTime timeZone] $procBody]
2050 return $procName
2053 #----------------------------------------------------------------------
2055 # LocaleNumeralMatcher --
2057 # Composes a regexp that captures the numerals in the given
2058 # locale, and a dictionary to map them to conventional numerals.
2060 # Parameters:
2061 # locale - Name of the current locale
2063 # Results:
2064 # Returns a two-element list comprising the regexp and the
2065 # dictionary.
2067 # Side effects:
2068 # Caches the result.
2070 #----------------------------------------------------------------------
2072 proc ::tcl::clock::LocaleNumeralMatcher {l} {
2074 variable LocaleNumeralCache
2076 if { ![dict exists $LocaleNumeralCache $l] } {
2077 set d {}
2078 set i 0
2079 set sep \(
2080 foreach n [mc LOCALE_NUMERALS] {
2081 dict set d $n $i
2082 regsub -all {[^[:alnum:]]} $n \\\\& subex
2083 append re $sep $subex
2084 set sep |
2085 incr i
2087 append re \)
2088 dict set LocaleNumeralCache $l [list $re $d]
2090 return [dict get $LocaleNumeralCache $l]
2095 #----------------------------------------------------------------------
2097 # UniquePrefixRegexp --
2099 # Composes a regexp that performs unique-prefix matching. The
2100 # RE matches one of a supplied set of strings, or any unique
2101 # prefix thereof.
2103 # Parameters:
2104 # data - List of alternating match-strings and values.
2105 # Match-strings with distinct values are considered
2106 # distinct.
2108 # Results:
2109 # Returns a two-element list. The first is a regexp that
2110 # matches any unique prefix of any of the strings. The second
2111 # is a dictionary whose keys are match values from the regexp
2112 # and whose values are the corresponding values from 'data'.
2114 # Side effects:
2115 # None.
2117 #----------------------------------------------------------------------
2119 proc ::tcl::clock::UniquePrefixRegexp { data } {
2121 # The 'successors' dictionary will contain, for each string that
2122 # is a prefix of any key, all characters that may follow that
2123 # prefix. The 'prefixMapping' dictionary will have keys that
2124 # are prefixes of keys and values that correspond to the keys.
2126 set prefixMapping [dict create]
2127 set successors [dict create {} {}]
2129 # Walk the key-value pairs
2131 foreach { key value } $data {
2133 # Construct all prefixes of the key;
2135 set prefix {}
2136 foreach char [split $key {}] {
2137 set oldPrefix $prefix
2138 dict set successors $oldPrefix $char {}
2139 append prefix $char
2141 # Put the prefixes in the 'prefixMapping' and 'successors'
2142 # dictionaries
2144 dict lappend prefixMapping $prefix $value
2145 if { ![dict exists $successors $prefix] } {
2146 dict set successors $prefix {}
2151 # Identify those prefixes that designate unique values, and
2152 # those that are the full keys
2154 set uniquePrefixMapping {}
2155 dict for { key valueList } $prefixMapping {
2156 if { [llength $valueList] == 1 } {
2157 dict set uniquePrefixMapping $key [lindex $valueList 0]
2160 foreach { key value } $data {
2161 dict set uniquePrefixMapping $key $value
2164 # Construct the re.
2166 return [list \
2167 [MakeUniquePrefixRegexp $successors $uniquePrefixMapping {}] \
2168 $uniquePrefixMapping]
2171 #----------------------------------------------------------------------
2173 # MakeUniquePrefixRegexp --
2175 # Service procedure for 'UniquePrefixRegexp' that constructs
2176 # a regular expresison that matches the unique prefixes.
2178 # Parameters:
2179 # successors - Dictionary whose keys are all prefixes
2180 # of keys passed to 'UniquePrefixRegexp' and whose
2181 # values are dictionaries whose keys are the characters
2182 # that may follow those prefixes.
2183 # uniquePrefixMapping - Dictionary whose keys are the unique
2184 # prefixes and whose values are not examined.
2185 # prefixString - Current prefix being processed.
2187 # Results:
2188 # Returns a constructed regular expression that matches the set
2189 # of unique prefixes beginning with the 'prefixString'.
2191 # Side effects:
2192 # None.
2194 #----------------------------------------------------------------------
2196 proc ::tcl::clock::MakeUniquePrefixRegexp { successors
2197 uniquePrefixMapping
2198 prefixString } {
2200 # Get the characters that may follow the current prefix string
2202 set schars [lsort -ascii [dict keys [dict get $successors $prefixString]]]
2203 if { [llength $schars] == 0 } {
2204 return {}
2207 # If there is more than one successor character, or if the current
2208 # prefix is a unique prefix, surround the generated re with non-capturing
2209 # parentheses.
2211 set re {}
2212 if { [dict exists $uniquePrefixMapping $prefixString]
2213 || [llength $schars] > 1 } {
2214 append re "(?:"
2217 # Generate a regexp that matches the successors.
2219 set sep ""
2220 foreach { c } $schars {
2221 set nextPrefix $prefixString$c
2222 regsub -all {[^[:alnum:]]} $c \\\\& rechar
2223 append re $sep $rechar \
2224 [MakeUniquePrefixRegexp \
2225 $successors $uniquePrefixMapping $nextPrefix]
2226 set sep |
2229 # If the current prefix is a unique prefix, make all following text
2230 # optional. Otherwise, if there is more than one successor character,
2231 # close the non-capturing parentheses.
2233 if { [dict exists $uniquePrefixMapping $prefixString] } {
2234 append re ")?"
2235 } elseif { [llength $schars] > 1 } {
2236 append re ")"
2239 return $re
2242 #----------------------------------------------------------------------
2244 # MakeParseCodeFromFields --
2246 # Composes Tcl code to extract the Julian Day Number from a
2247 # dictionary containing date fields.
2249 # Parameters:
2250 # dateFields -- Dictionary whose keys are fields of the date,
2251 # and whose values are the rightmost positions
2252 # at which those fields appear.
2253 # parseActions -- List of triples: field set, priority, and
2254 # code to emit. Smaller priorities are better, and
2255 # the list must be in ascending order by priority
2257 # Results:
2258 # Returns a burst of code that extracts the day number from the
2259 # given date.
2261 # Side effects:
2262 # None.
2264 #----------------------------------------------------------------------
2266 proc ::tcl::clock::MakeParseCodeFromFields { dateFields parseActions } {
2268 set currPrio 999
2269 set currFieldPos [list]
2270 set currCodeBurst {
2271 error "in ::tcl::clock::MakeParseCodeFromFields: can't happen"
2274 foreach { fieldSet prio parseAction } $parseActions {
2276 # If we've found an answer that's better than any that follow,
2277 # quit now.
2279 if { $prio > $currPrio } {
2280 break
2283 # Accumulate the field positions that are used in the current
2284 # field grouping.
2286 set fieldPos [list]
2287 set ok true
2288 foreach field $fieldSet {
2289 if { ! [dict exists $dateFields $field] } {
2290 set ok 0
2291 break
2293 lappend fieldPos [dict get $dateFields $field]
2296 # Quit if we don't have a complete set of fields
2297 if { !$ok } {
2298 continue
2301 # Determine whether the current answer is better than the last.
2303 set fPos [lsort -integer -decreasing $fieldPos]
2305 if { $prio == $currPrio } {
2306 foreach currPos $currFieldPos newPos $fPos {
2307 if { ![string is integer $newPos]
2308 || ![string is integer $currPos]
2309 || $newPos > $currPos } {
2310 break
2312 if { $newPos < $currPos } {
2313 set ok 0
2314 break
2318 if { !$ok } {
2319 continue
2322 # Remember the best possibility for extracting date information
2324 set currPrio $prio
2325 set currFieldPos $fPos
2326 set currCodeBurst $parseAction
2330 return $currCodeBurst
2334 #----------------------------------------------------------------------
2336 # EnterLocale --
2338 # Switch [mclocale] to a given locale if necessary
2340 # Parameters:
2341 # locale -- Desired locale
2342 # oldLocaleVar -- Name of a variable in caller's scope that
2343 # tracks the previous locale name.
2345 # Results:
2346 # Returns the locale that was previously current.
2348 # Side effects:
2349 # Does [mclocale]. If necessary, uses [mcload] to load the
2350 # designated locale's files, and tracks that it has done so
2351 # in the 'McLoaded' variable.
2353 #----------------------------------------------------------------------
2355 proc ::tcl::clock::EnterLocale { locale oldLocaleVar } {
2357 upvar 1 $oldLocaleVar oldLocale
2359 variable MsgDir
2360 variable McLoaded
2362 set oldLocale [mclocale]
2363 if { $locale eq {system} } {
2365 if { $::tcl_platform(platform) ne {windows} } {
2367 # On a non-windows platform, the 'system' locale is
2368 # the same as the 'current' locale
2370 set locale current
2371 } else {
2373 # On a windows platform, the 'system' locale is
2374 # adapted from the 'current' locale by applying the
2375 # date and time formats from the Control Panel.
2376 # First, load the 'current' locale if it's not yet loaded
2378 if {![dict exists $McLoaded $oldLocale] } {
2379 mcload $MsgDir
2380 dict set McLoaded $oldLocale {}
2383 # Make a new locale string for the system locale, and
2384 # get the Control Panel information
2386 set locale ${oldLocale}_windows
2387 if { ![dict exists $McLoaded $locale] } {
2388 LoadWindowsDateTimeFormats $locale
2389 dict set McLoaded $locale {}
2393 if { $locale eq {current}} {
2394 set locale $oldLocale
2395 unset oldLocale
2396 } elseif { $locale eq $oldLocale } {
2397 unset oldLocale
2398 } else {
2399 mclocale $locale
2401 if { ![dict exists $McLoaded $locale] } {
2402 mcload $MsgDir
2403 dict set McLoaded $locale {}
2408 #----------------------------------------------------------------------
2410 # LoadWindowsDateTimeFormats --
2412 # Load the date/time formats from the Control Panel in Windows
2413 # and convert them so that they're usable by Tcl.
2415 # Parameters:
2416 # locale - Name of the locale in whose message catalog
2417 # the converted formats are to be stored.
2419 # Results:
2420 # None.
2422 # Side effects:
2423 # Updates the given message catalog with the locale strings.
2425 # Presumes that on entry, [mclocale] is set to the current locale,
2426 # so that default strings can be obtained if the Registry query
2427 # fails.
2429 #----------------------------------------------------------------------
2431 proc ::tcl::clock::LoadWindowsDateTimeFormats { locale } {
2433 # Bail out if we can't find the Registry
2435 variable NoRegistry
2436 if { [info exists NoRegistry] } return
2438 if { ![catch {
2439 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2440 sShortDate
2441 } string] } {
2442 set quote {}
2443 set datefmt {}
2444 foreach { unquoted quoted } [split $string '] {
2445 append datefmt $quote [string map {
2446 dddd %A
2447 ddd %a
2448 dd %d
2449 d %e
2450 MMMM %B
2451 MMM %b
2452 MM %m
2453 M %N
2454 yyyy %Y
2455 yy %y
2456 y %y
2457 gg {}
2458 } $unquoted]
2459 if { $quoted eq {} } {
2460 set quote '
2461 } else {
2462 set quote $quoted
2465 ::msgcat::mcset $locale DATE_FORMAT $datefmt
2468 if { ![catch {
2469 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2470 sLongDate
2471 } string] } {
2472 set quote {}
2473 set ldatefmt {}
2474 foreach { unquoted quoted } [split $string '] {
2475 append ldatefmt $quote [string map {
2476 dddd %A
2477 ddd %a
2478 dd %d
2479 d %e
2480 MMMM %B
2481 MMM %b
2482 MM %m
2483 M %N
2484 yyyy %Y
2485 yy %y
2486 y %y
2487 gg {}
2488 } $unquoted]
2489 if { $quoted eq {} } {
2490 set quote '
2491 } else {
2492 set quote $quoted
2495 ::msgcat::mcset $locale LOCALE_DATE_FORMAT $ldatefmt
2498 if { ![catch {
2499 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2500 sTimeFormat
2501 } string] } {
2502 set quote {}
2503 set timefmt {}
2504 foreach { unquoted quoted } [split $string '] {
2505 append timefmt $quote [string map {
2506 HH %H
2507 H %k
2508 hh %I
2509 h %l
2510 mm %M
2511 m %M
2512 ss %S
2513 s %S
2514 tt %p
2515 t %p
2516 } $unquoted]
2517 if { $quoted eq {} } {
2518 set quote '
2519 } else {
2520 set quote $quoted
2523 ::msgcat::mcset $locale TIME_FORMAT $timefmt
2526 catch {
2527 ::msgcat::mcset $locale DATE_TIME_FORMAT "$datefmt $timefmt"
2529 catch {
2530 ::msgcat::mcset $locale LOCALE_DATE_TIME_FORMAT "$ldatefmt $timefmt"
2533 return
2537 #----------------------------------------------------------------------
2539 # LocalizeFormat --
2541 # Map away locale-dependent format groups in a clock format.
2543 # Parameters:
2544 # locale -- Current [mclocale] locale, supplied to avoid
2545 # an extra call
2546 # format -- Format supplied to [clock scan] or [clock format]
2548 # Results:
2549 # Returns the string with locale-dependent composite format
2550 # groups substituted out.
2552 # Side effects:
2553 # None.
2555 #----------------------------------------------------------------------
2557 proc ::tcl::clock::LocalizeFormat { locale format } {
2559 variable McLoaded
2561 if { [dict exists $McLoaded $locale FORMAT $format] } {
2562 return [dict get $McLoaded $locale FORMAT $format]
2564 set inFormat $format
2566 # Handle locale-dependent format groups by mapping them out of the format
2567 # string. Note that the order of the [string map] operations is
2568 # significant because later formats can refer to later ones; for example
2569 # %c can refer to %X, which in turn can refer to %T.
2571 set list {
2572 %% %%
2573 %D %m/%d/%Y
2574 %+ {%a %b %e %H:%M:%S %Z %Y}
2576 lappend list %EY [string map $list [mc LOCALE_YEAR_FORMAT]]
2577 lappend list %T [string map $list [mc TIME_FORMAT_24_SECS]]
2578 lappend list %R [string map $list [mc TIME_FORMAT_24]]
2579 lappend list %r [string map $list [mc TIME_FORMAT_12]]
2580 lappend list %X [string map $list [mc TIME_FORMAT]]
2581 lappend list %EX [string map $list [mc LOCALE_TIME_FORMAT]]
2582 lappend list %x [string map $list [mc DATE_FORMAT]]
2583 lappend list %Ex [string map $list [mc LOCALE_DATE_FORMAT]]
2584 lappend list %c [string map $list [mc DATE_TIME_FORMAT]]
2585 lappend list %Ec [string map $list [mc LOCALE_DATE_TIME_FORMAT]]
2586 set format [string map $list $format]
2588 dict set McLoaded $locale FORMAT $inFormat $format
2589 return $format
2592 #----------------------------------------------------------------------
2594 # FormatNumericTimeZone --
2596 # Formats a time zone as +hhmmss
2598 # Parameters:
2599 # z - Time zone in seconds east of Greenwich
2601 # Results:
2602 # Returns the time zone formatted in a numeric form
2604 # Side effects:
2605 # None.
2607 #----------------------------------------------------------------------
2609 proc ::tcl::clock::FormatNumericTimeZone { z } {
2611 if { $z < 0 } {
2612 set z [expr { - $z }]
2613 set retval -
2614 } else {
2615 set retval +
2617 append retval [::format %02d [expr { $z / 3600 }]]
2618 set z [expr { $z % 3600 }]
2619 append retval [::format %02d [expr { $z / 60 }]]
2620 set z [expr { $z % 60 }]
2621 if { $z != 0 } {
2622 append retval [::format %02d $z]
2624 return $retval
2628 #----------------------------------------------------------------------
2630 # FormatStarDate --
2632 # Formats a date as a StarDate.
2634 # Parameters:
2635 # date - Dictionary containing 'year', 'dayOfYear', and
2636 # 'localSeconds' fields.
2638 # Results:
2639 # Returns the given date formatted as a StarDate.
2641 # Side effects:
2642 # None.
2644 # Jeff Hobbs put this in to support an atrocious pun about Tcl being
2645 # "Enterprise ready." Now we're stuck with it.
2647 #----------------------------------------------------------------------
2649 proc ::tcl::clock::FormatStarDate { date } {
2651 variable Roddenberry
2653 # Get day of year, zero based
2655 set doy [expr { [dict get $date dayOfYear] - 1 }]
2657 # Determine whether the year is a leap year
2659 set lp [IsGregorianLeapYear $date]
2661 # Convert day of year to a fractional year
2663 if { $lp } {
2664 set fractYear [expr { 1000 * $doy / 366 }]
2665 } else {
2666 set fractYear [expr { 1000 * $doy / 365 }]
2669 # Put together the StarDate
2671 return [::format "Stardate %02d%03d.%1d" \
2672 [expr { [dict get $date year] - $Roddenberry }] \
2673 $fractYear \
2674 [expr { [dict get $date localSeconds] % 86400
2675 / ( 86400 / 10 ) }]]
2678 #----------------------------------------------------------------------
2680 # ParseStarDate --
2682 # Parses a StarDate
2684 # Parameters:
2685 # year - Year from the Roddenberry epoch
2686 # fractYear - Fraction of a year specifiying the day of year.
2687 # fractDay - Fraction of a day
2689 # Results:
2690 # Returns a count of seconds from the Posix epoch.
2692 # Side effects:
2693 # None.
2695 # Jeff Hobbs put this in to support an atrocious pun about Tcl being
2696 # "Enterprise ready." Now we're stuck with it.
2698 #----------------------------------------------------------------------
2700 proc ::tcl::clock::ParseStarDate { year fractYear fractDay } {
2702 variable Roddenberry
2704 # Build a tentative date from year and fraction.
2706 set date [dict create \
2707 gregorian 1 \
2708 era CE \
2709 year [expr { $year + $Roddenberry }] \
2710 dayOfYear [expr { $fractYear * 365 / 1000 + 1 }]]
2711 set date [GetJulianDayFromGregorianEraYearDay $date[set date {}]]
2713 # Determine whether the given year is a leap year
2715 set lp [IsGregorianLeapYear $date]
2717 # Reconvert the fractional year according to whether the given
2718 # year is a leap year
2720 if { $lp } {
2721 dict set date dayOfYear \
2722 [expr { $fractYear * 366 / 1000 + 1 }]
2723 } else {
2724 dict set date dayOfYear \
2725 [expr { $fractYear * 365 / 1000 + 1 }]
2727 dict unset date julianDay
2728 dict unset date gregorian
2729 set date [GetJulianDayFromGregorianEraYearDay $date[set date {}]]
2731 return [expr { 86400 * [dict get $date julianDay]
2732 - 210866803200
2733 + ( 86400 / 10 ) * $fractDay }]
2737 #----------------------------------------------------------------------
2739 # ScanWide --
2741 # Scans a wide integer from an input
2743 # Parameters:
2744 # str - String containing a decimal wide integer
2746 # Results:
2747 # Returns the string as a pure wide integer. Throws an error if
2748 # the string is misformatted or out of range.
2750 #----------------------------------------------------------------------
2752 proc ::tcl::clock::ScanWide { str } {
2753 set count [::scan $str {%ld %c} result junk]
2754 if { $count != 1 } {
2755 return -code error -errorcode [list CLOCK notAnInteger $str] \
2756 "\"$str\" is not an integer"
2758 if { [incr result 0] != $str } {
2759 return -code error -errorcode [list CLOCK integervalueTooLarge] \
2760 "integer value too large to represent"
2762 return $result
2765 #----------------------------------------------------------------------
2767 # InterpretTwoDigitYear --
2769 # Given a date that contains only the year of the century,
2770 # determines the target value of a two-digit year.
2772 # Parameters:
2773 # date - Dictionary containing fields of the date.
2774 # baseTime - Base time relative to which the date is expressed.
2775 # twoDigitField - Name of the field that stores the two-digit year.
2776 # Default is 'yearOfCentury'
2777 # fourDigitField - Name of the field that will receive the four-digit
2778 # year. Default is 'year'
2780 # Results:
2781 # Returns the dictionary augmented with the four-digit year, stored in
2782 # the given key.
2784 # Side effects:
2785 # None.
2787 # The current rule for interpreting a two-digit year is that the year
2788 # shall be between 1937 and 2037, thus staying within the range of a
2789 # 32-bit signed value for time. This rule may change to a sliding
2790 # window in future versions, so the 'baseTime' parameter (which is
2791 # currently ignored) is provided in the procedure signature.
2793 #----------------------------------------------------------------------
2795 proc ::tcl::clock::InterpretTwoDigitYear { date baseTime
2796 { twoDigitField yearOfCentury }
2797 { fourDigitField year } } {
2799 set yr [dict get $date $twoDigitField]
2800 if { $yr <= 37 } {
2801 dict set date $fourDigitField [expr { $yr + 2000 }]
2802 } else {
2803 dict set date $fourDigitField [expr { $yr + 1900 }]
2805 return $date
2809 #----------------------------------------------------------------------
2811 # AssignBaseYear --
2813 # Places the number of the current year into a dictionary.
2815 # Parameters:
2816 # date - Dictionary value to update
2817 # baseTime - Base time from which to extract the year, expressed
2818 # in seconds from the Posix epoch
2819 # timezone - the time zone in which the date is being scanned
2820 # changeover - the Julian Day on which the Gregorian calendar
2821 # was adopted in the target locale.
2823 # Results:
2824 # Returns the dictionary with the current year assigned.
2826 # Side effects:
2827 # None.
2829 #----------------------------------------------------------------------
2831 proc ::tcl::clock::AssignBaseYear { date baseTime timezone changeover } {
2833 variable TZData
2835 # Find the Julian Day Number corresponding to the base time, and
2836 # find the Gregorian year corresponding to that Julian Day.
2838 set date2 [GetDateFields $baseTime $TZData($timezone) $changeover]
2840 # Store the converted year
2842 dict set date era [dict get $date2 era]
2843 dict set date year [dict get $date2 year]
2845 return $date
2849 #----------------------------------------------------------------------
2851 # AssignBaseIso8601Year --
2853 # Determines the base year in the ISO8601 fiscal calendar.
2855 # Parameters:
2856 # date - Dictionary containing the fields of the date that
2857 # is to be augmented with the base year.
2858 # baseTime - Base time expressed in seconds from the Posix epoch.
2859 # timeZone - Target time zone
2860 # changeover - Julian Day of adoption of the Gregorian calendar in
2861 # the target locale.
2863 # Results:
2864 # Returns the given date with "iso8601Year" set to the
2865 # base year.
2867 # Side effects:
2868 # None.
2870 #----------------------------------------------------------------------
2872 proc ::tcl::clock::AssignBaseIso8601Year {date baseTime timeZone changeover} {
2874 variable TZData
2876 # Find the Julian Day Number corresponding to the base time
2878 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2880 # Calculate the ISO8601 date and transfer the year
2882 dict set date era CE
2883 dict set date iso8601Year [dict get $date2 iso8601Year]
2884 return $date
2887 #----------------------------------------------------------------------
2889 # AssignBaseMonth --
2891 # Places the number of the current year and month into a
2892 # dictionary.
2894 # Parameters:
2895 # date - Dictionary value to update
2896 # baseTime - Time from which the year and month are to be
2897 # obtained, expressed in seconds from the Posix epoch.
2898 # timezone - Name of the desired time zone
2899 # changeover - Julian Day on which the Gregorian calendar was adopted.
2901 # Results:
2902 # Returns the dictionary with the base year and month assigned.
2904 # Side effects:
2905 # None.
2907 #----------------------------------------------------------------------
2909 proc ::tcl::clock::AssignBaseMonth {date baseTime timezone changeover} {
2911 variable TZData
2913 # Find the year and month corresponding to the base time
2915 set date2 [GetDateFields $baseTime $TZData($timezone) $changeover]
2916 dict set date era [dict get $date2 era]
2917 dict set date year [dict get $date2 year]
2918 dict set date month [dict get $date2 month]
2919 return $date
2923 #----------------------------------------------------------------------
2925 # AssignBaseWeek --
2927 # Determines the base year and week in the ISO8601 fiscal calendar.
2929 # Parameters:
2930 # date - Dictionary containing the fields of the date that
2931 # is to be augmented with the base year and week.
2932 # baseTime - Base time expressed in seconds from the Posix epoch.
2933 # changeover - Julian Day on which the Gregorian calendar was adopted
2934 # in the target locale.
2936 # Results:
2937 # Returns the given date with "iso8601Year" set to the
2938 # base year and "iso8601Week" to the week number.
2940 # Side effects:
2941 # None.
2943 #----------------------------------------------------------------------
2945 proc ::tcl::clock::AssignBaseWeek {date baseTime timeZone changeover} {
2947 variable TZData
2949 # Find the Julian Day Number corresponding to the base time
2951 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2953 # Calculate the ISO8601 date and transfer the year
2955 dict set date era CE
2956 dict set date iso8601Year [dict get $date2 iso8601Year]
2957 dict set date iso8601Week [dict get $date2 iso8601Week]
2958 return $date
2961 #----------------------------------------------------------------------
2963 # AssignBaseJulianDay --
2965 # Determines the base day for a time-of-day conversion.
2967 # Parameters:
2968 # date - Dictionary that is to get the base day
2969 # baseTime - Base time expressed in seconds from the Posix epoch
2970 # changeover - Julian day on which the Gregorian calendar was
2971 # adpoted in the target locale.
2973 # Results:
2974 # Returns the given dictionary augmented with a 'julianDay' field
2975 # that contains the base day.
2977 # Side effects:
2978 # None.
2980 #----------------------------------------------------------------------
2982 proc ::tcl::clock::AssignBaseJulianDay { date baseTime timeZone changeover } {
2984 variable TZData
2986 # Find the Julian Day Number corresponding to the base time
2988 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2989 dict set date julianDay [dict get $date2 julianDay]
2991 return $date
2994 #----------------------------------------------------------------------
2996 # InterpretHMSP --
2998 # Interprets a time in the form "hh:mm:ss am".
3000 # Parameters:
3001 # date -- Dictionary containing "hourAMPM", "minute", "second"
3002 # and "amPmIndicator" fields.
3004 # Results:
3005 # Returns the number of seconds from local midnight.
3007 # Side effects:
3008 # None.
3010 #----------------------------------------------------------------------
3012 proc ::tcl::clock::InterpretHMSP { date } {
3014 set hr [dict get $date hourAMPM]
3015 if { $hr == 12 } {
3016 set hr 0
3018 if { [dict get $date amPmIndicator] } {
3019 incr hr 12
3021 dict set date hour $hr
3022 return [InterpretHMS $date[set date {}]]
3026 #----------------------------------------------------------------------
3028 # InterpretHMS --
3030 # Interprets a 24-hour time "hh:mm:ss"
3032 # Parameters:
3033 # date -- Dictionary containing the "hour", "minute" and "second"
3034 # fields.
3036 # Results:
3037 # Returns the given dictionary augmented with a "secondOfDay"
3038 # field containing the number of seconds from local midnight.
3040 # Side effects:
3041 # None.
3043 #----------------------------------------------------------------------
3045 proc ::tcl::clock::InterpretHMS { date } {
3047 return [expr { ( [dict get $date hour] * 60
3048 + [dict get $date minute] ) * 60
3049 + [dict get $date second] }]
3053 #----------------------------------------------------------------------
3055 # GetSystemTimeZone --
3057 # Determines the system time zone, which is the default for the
3058 # 'clock' command if no other zone is supplied.
3060 # Parameters:
3061 # None.
3063 # Results:
3064 # Returns the system time zone.
3066 # Side effects:
3067 # Stores the sustem time zone in the 'CachedSystemTimeZone'
3068 # variable, since determining it may be an expensive process.
3070 #----------------------------------------------------------------------
3072 proc ::tcl::clock::GetSystemTimeZone {} {
3074 variable CachedSystemTimeZone
3075 variable TimeZoneBad
3077 if {[set result [getenv TCL_TZ]] ne {}} {
3078 set timezone $result
3079 } elseif {[set result [getenv TZ]] ne {}} {
3080 set timezone $result
3081 } elseif { [info exists CachedSystemTimeZone] } {
3082 set timezone $CachedSystemTimeZone
3083 } elseif { $::tcl_platform(platform) eq {windows} } {
3084 set timezone [GuessWindowsTimeZone]
3085 } elseif { [file exists /etc/localtime]
3086 && ![catch {ReadZoneinfoFile \
3087 Tcl/Localtime /etc/localtime}] } {
3088 set timezone :Tcl/Localtime
3089 } else {
3090 set timezone :localtime
3092 set CachedSystemTimeZone $timezone
3093 if { ![dict exists $TimeZoneBad $timezone] } {
3094 dict set TimeZoneBad $timezone [catch {SetupTimeZone $timezone}]
3096 if { [dict get $TimeZoneBad $timezone] } {
3097 return :localtime
3098 } else {
3099 return $timezone
3104 #----------------------------------------------------------------------
3106 # ConvertLegacyTimeZone --
3108 # Given an alphanumeric time zone identifier and the system
3109 # time zone, convert the alphanumeric identifier to an
3110 # unambiguous time zone.
3112 # Parameters:
3113 # tzname - Name of the time zone to convert
3115 # Results:
3116 # Returns a time zone name corresponding to tzname, but
3117 # in an unambiguous form, generally +hhmm.
3119 # This procedure is implemented primarily to allow the parsing of
3120 # RFC822 date/time strings. Processing a time zone name on input
3121 # is not recommended practice, because there is considerable room
3122 # for ambiguity; for instance, is BST Brazilian Standard Time, or
3123 # British Summer Time?
3125 #----------------------------------------------------------------------
3127 proc ::tcl::clock::ConvertLegacyTimeZone { tzname } {
3129 variable LegacyTimeZone
3131 set tzname [string tolower $tzname]
3132 if { ![dict exists $LegacyTimeZone $tzname] } {
3133 return -code error -errorcode [list CLOCK badTZName $tzname] \
3134 "time zone \"$tzname\" not found"
3135 } else {
3136 return [dict get $LegacyTimeZone $tzname]
3141 #----------------------------------------------------------------------
3143 # SetupTimeZone --
3145 # Given the name or specification of a time zone, sets up
3146 # its in-memory data.
3148 # Parameters:
3149 # tzname - Name of a time zone
3151 # Results:
3152 # Unless the time zone is ':localtime', sets the TZData array
3153 # to contain the lookup table for local<->UTC conversion.
3154 # Returns an error if the time zone cannot be parsed.
3156 #----------------------------------------------------------------------
3158 proc ::tcl::clock::SetupTimeZone { timezone } {
3160 variable TZData
3162 if {! [info exists TZData($timezone)] } {
3163 variable MINWIDE
3164 if { $timezone eq {:localtime} } {
3166 # Nothing to do, we'll convert using the localtime function
3168 } elseif { [regexp {^([-+])(\d\d)(?::?(\d\d)(?::?(\d\d))?)?} $timezone \
3169 -> s hh mm ss] } {
3171 # Make a fixed offset
3173 ::scan $hh %d hh
3174 if { $mm eq {} } {
3175 set mm 0
3176 } else {
3177 ::scan $mm %d mm
3179 if { $ss eq {} } {
3180 set ss 0
3181 } else {
3182 ::scan $ss %d ss
3184 set offset [expr { ( $hh * 60 + $mm ) * 60 + $ss }]
3185 if { $s eq {-} } {
3186 set offset [expr { - $offset }]
3188 set TZData($timezone) [list [list $MINWIDE $offset -1 $timezone]]
3190 } elseif { [string index $timezone 0] eq {:} } {
3192 # Convert using a time zone file
3194 if {
3195 [catch {
3196 LoadTimeZoneFile [string range $timezone 1 end]
3198 && [catch {
3199 LoadZoneinfoFile [string range $timezone 1 end]
3202 return -code error \
3203 -errorcode [list CLOCK badTimeZone $timezone] \
3204 "time zone \"$timezone\" not found"
3207 } elseif { ![catch {ParsePosixTimeZone $timezone} tzfields] } {
3209 # This looks like a POSIX time zone - try to process it
3211 if { [catch {ProcessPosixTimeZone $tzfields} data opts] } {
3212 if { [lindex [dict get $opts -errorcode] 0] eq {CLOCK} } {
3213 dict unset opts -errorinfo
3215 return -options $opts $data
3216 } else {
3217 set TZData($timezone) $data
3220 } else {
3222 # We couldn't parse this as a POSIX time zone. Try
3223 # again with a time zone file - this time without a colon
3225 if { [catch { LoadTimeZoneFile $timezone }]
3226 && [catch { LoadZoneinfoFile $timezone } - opts] } {
3227 dict unset opts -errorinfo
3228 return -options $opts "time zone $timezone not found"
3230 set TZData($timezone) $TZData(:$timezone)
3234 return
3237 #----------------------------------------------------------------------
3239 # GuessWindowsTimeZone --
3241 # Determines the system time zone on windows.
3243 # Parameters:
3244 # None.
3246 # Results:
3247 # Returns a time zone specifier that corresponds to the system
3248 # time zone information found in the Registry.
3250 # Bugs:
3251 # Fixed dates for DST change are unimplemented at present, because
3252 # no time zone information supplied with Windows actually uses
3253 # them!
3255 # On a Windows system where neither $env(TCL_TZ) nor $env(TZ) is
3256 # specified, GuessWindowsTimeZone looks in the Registry for the
3257 # system time zone information. It then attempts to find an entry
3258 # in WinZoneInfo for a time zone that uses the same rules. If
3259 # it finds one, it returns it; otherwise, it constructs a Posix-style
3260 # time zone string and returns that.
3262 #----------------------------------------------------------------------
3264 proc ::tcl::clock::GuessWindowsTimeZone {} {
3266 variable WinZoneInfo
3267 variable NoRegistry
3268 variable TimeZoneBad
3270 if { [info exists NoRegistry] } {
3271 return :localtime
3274 # Dredge time zone information out of the registry
3276 if { [catch {
3277 set rpath HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\TimeZoneInformation
3278 set data [list \
3279 [expr { -60
3280 * [registry get $rpath Bias] }] \
3281 [expr { -60
3282 * [registry get $rpath StandardBias] }] \
3283 [expr { -60 \
3284 * [registry get $rpath DaylightBias] }]]
3285 set stdtzi [registry get $rpath StandardStart]
3286 foreach ind {0 2 14 4 6 8 10 12} {
3287 binary scan $stdtzi @${ind}s val
3288 lappend data $val
3290 set daytzi [registry get $rpath DaylightStart]
3291 foreach ind {0 2 14 4 6 8 10 12} {
3292 binary scan $daytzi @${ind}s val
3293 lappend data $val
3295 }] } {
3297 # Missing values in the Registry - bail out
3299 return :localtime
3302 # Make up a Posix time zone specifier if we can't find one.
3303 # Check here that the tzdata file exists, in case we're running
3304 # in an environment (e.g. starpack) where tzdata is incomplete.
3305 # (Bug 1237907)
3307 if { [dict exists $WinZoneInfo $data] } {
3308 set tzname [dict get $WinZoneInfo $data]
3309 if { ! [dict exists $TimeZoneBad $tzname] } {
3310 dict set TimeZoneBad $tzname [catch {SetupTimeZone $tzname}]
3312 } else {
3313 set tzname {}
3315 if { $tzname eq {} || [dict get $TimeZoneBad $tzname] } {
3316 lassign $data \
3317 bias stdBias dstBias \
3318 stdYear stdMonth stdDayOfWeek stdDayOfMonth \
3319 stdHour stdMinute stdSecond stdMillisec \
3320 dstYear dstMonth dstDayOfWeek dstDayOfMonth \
3321 dstHour dstMinute dstSecond dstMillisec
3322 set stdDelta [expr { $bias + $stdBias }]
3323 set dstDelta [expr { $bias + $dstBias }]
3324 if { $stdDelta <= 0 } {
3325 set stdSignum +
3326 set stdDelta [expr { - $stdDelta }]
3327 set dispStdSignum -
3328 } else {
3329 set stdSignum -
3330 set dispStdSignum +
3332 set hh [::format %02d [expr { $stdDelta / 3600 }]]
3333 set mm [::format %02d [expr { ($stdDelta / 60 ) % 60 }]]
3334 set ss [::format %02d [expr { $stdDelta % 60 }]]
3335 set tzname {}
3336 append tzname < $dispStdSignum $hh $mm > $stdSignum $hh : $mm : $ss
3337 if { $stdMonth >= 0 } {
3338 if { $dstDelta <= 0 } {
3339 set dstSignum +
3340 set dstDelta [expr { - $dstDelta }]
3341 set dispDstSignum -
3342 } else {
3343 set dstSignum -
3344 set dispDstSignum +
3346 set hh [::format %02d [expr { $dstDelta / 3600 }]]
3347 set mm [::format %02d [expr { ($dstDelta / 60 ) % 60 }]]
3348 set ss [::format %02d [expr { $dstDelta % 60 }]]
3349 append tzname < $dispDstSignum $hh $mm > $dstSignum $hh : $mm : $ss
3350 if { $dstYear == 0 } {
3351 append tzname ,M $dstMonth . $dstDayOfMonth . $dstDayOfWeek
3352 } else {
3353 # I have not been able to find any locale on which
3354 # Windows converts time zone on a fixed day of the year,
3355 # hence don't know how to interpret the fields.
3356 # If someone can inform me, I'd be glad to code it up.
3357 # For right now, we bail out in such a case.
3358 return :localtime
3360 append tzname / [::format %02d $dstHour] \
3361 : [::format %02d $dstMinute] \
3362 : [::format %02d $dstSecond]
3363 if { $stdYear == 0 } {
3364 append tzname ,M $stdMonth . $stdDayOfMonth . $stdDayOfWeek
3365 } else {
3366 # I have not been able to find any locale on which
3367 # Windows converts time zone on a fixed day of the year,
3368 # hence don't know how to interpret the fields.
3369 # If someone can inform me, I'd be glad to code it up.
3370 # For right now, we bail out in such a case.
3371 return :localtime
3373 append tzname / [::format %02d $stdHour] \
3374 : [::format %02d $stdMinute] \
3375 : [::format %02d $stdSecond]
3377 dict set WinZoneInfo $data $tzname
3380 return [dict get $WinZoneInfo $data]
3384 #----------------------------------------------------------------------
3386 # LoadTimeZoneFile --
3388 # Load the data file that specifies the conversion between a
3389 # given time zone and Greenwich.
3391 # Parameters:
3392 # fileName -- Name of the file to load
3394 # Results:
3395 # None.
3397 # Side effects:
3398 # TZData(:fileName) contains the time zone data
3400 #----------------------------------------------------------------------
3402 proc ::tcl::clock::LoadTimeZoneFile { fileName } {
3403 variable DataDir
3404 variable TZData
3406 if { [info exists TZData($fileName)] } {
3407 return
3410 # Since an unsafe interp uses the [clock] command in the master,
3411 # this code is security sensitive. Make sure that the path name
3412 # cannot escape the given directory.
3414 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3415 return -code error \
3416 -errorcode [list CLOCK badTimeZone $:fileName] \
3417 "time zone \":$fileName\" not valid"
3419 if { [catch {
3420 source -encoding utf-8 [file join $DataDir $fileName]
3421 }] } {
3422 return -code error \
3423 -errorcode [list CLOCK badTimeZone :$fileName] \
3424 "time zone \":$fileName\" not found"
3426 return
3429 #----------------------------------------------------------------------
3431 # LoadZoneinfoFile --
3433 # Loads a binary time zone information file in Olson format.
3435 # Parameters:
3436 # fileName - Relative path name of the file to load.
3438 # Results:
3439 # Returns an empty result normally; returns an error if no
3440 # Olson file was found or the file was malformed in some way.
3442 # Side effects:
3443 # TZData(:fileName) contains the time zone data
3445 #----------------------------------------------------------------------
3447 proc ::tcl::clock::LoadZoneinfoFile { fileName } {
3449 variable ZoneinfoPaths
3451 # Since an unsafe interp uses the [clock] command in the master,
3452 # this code is security sensitive. Make sure that the path name
3453 # cannot escape the given directory.
3455 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3456 return -code error \
3457 -errorcode [list CLOCK badTimeZone $:fileName] \
3458 "time zone \":$fileName\" not valid"
3460 foreach d $ZoneinfoPaths {
3461 set fname [file join $d $fileName]
3462 if { [file readable $fname] && [file isfile $fname] } {
3463 break
3465 unset fname
3467 ReadZoneinfoFile $fileName $fname
3470 #----------------------------------------------------------------------
3472 # ReadZoneinfoFile --
3474 # Loads a binary time zone information file in Olson format.
3476 # Parameters:
3477 # fileName - Name of the time zone (relative path name of the
3478 # file).
3479 # fname - Absolute path name of the file.
3481 # Results:
3482 # Returns an empty result normally; returns an error if no
3483 # Olson file was found or the file was malformed in some way.
3485 # Side effects:
3486 # TZData(:fileName) contains the time zone data
3488 #----------------------------------------------------------------------
3491 proc ::tcl::clock::ReadZoneinfoFile {fileName fname} {
3492 variable MINWIDE
3493 variable TZData
3494 if { ![info exists fname] } {
3495 return -code error "$fileName not found"
3498 if { [file size $fname] > 262144 } {
3499 return -code error "$fileName too big"
3502 # Suck in all the data from the file
3504 set f [open $fname r]
3505 fconfigure $f -translation binary
3506 set d [read $f]
3507 close $f
3509 # The file begins with a magic number, sixteen reserved bytes,
3510 # and then six 4-byte integers giving counts of fileds in the file.
3512 binary scan $d a4a1x15IIIIII \
3513 magic version nIsGMT nIsStd nLeap nTime nType nChar
3514 set seek 44
3515 set ilen 4
3516 set iformat I
3517 if { $magic != {TZif} } {
3518 return -code error "$fileName not a time zone information file"
3520 if { $nType > 255 } {
3521 return -code error "$fileName contains too many time types"
3523 # Accept only Posix-style zoneinfo. Sorry, 'leaps' bigots.
3524 if { $nLeap != 0 } {
3525 return -code error "$fileName contains leap seconds"
3528 # In a version 2 file, we use the second part of the file, which
3529 # contains 64-bit transition times.
3531 if {$version eq "2"} {
3532 set seek [expr {44
3533 + 5 * $nTime
3534 + 6 * $nType
3535 + 4 * $nLeap
3536 + $nIsStd
3537 + $nIsGMT
3538 + $nChar
3540 binary scan $d @${seek}a4a1x15IIIIII \
3541 magic version nIsGMT nIsStd nLeap nTime nType nChar
3542 if {$magic ne {TZif}} {
3543 return -code error "seek address $seek miscomputed, magic = $magic"
3545 set iformat W
3546 set ilen 8
3547 incr seek 44
3550 # Next come ${nTime} transition times, followed by ${nTime} time type
3551 # codes. The type codes are unsigned 1-byte quantities. We insert an
3552 # arbitrary start time in front of the transitions.
3554 binary scan $d @${seek}${iformat}${nTime}c${nTime} times tempCodes
3555 incr seek [expr { ($ilen + 1) * $nTime }]
3556 set times [linsert $times 0 $MINWIDE]
3557 set codes {}
3558 foreach c $tempCodes {
3559 lappend codes [expr { $c & 0xff }]
3561 set codes [linsert $codes 0 0]
3563 # Next come ${nType} time type descriptions, each of which has an
3564 # offset (seconds east of GMT), a DST indicator, and an index into
3565 # the abbreviation text.
3567 for { set i 0 } { $i < $nType } { incr i } {
3568 binary scan $d @${seek}Icc gmtOff isDst abbrInd
3569 lappend types [list $gmtOff $isDst $abbrInd]
3570 incr seek 6
3573 # Next come $nChar characters of time zone name abbreviations,
3574 # which are null-terminated.
3575 # We build them up into a dictionary indexed by character index,
3576 # because that's what's in the indices above.
3578 binary scan $d @${seek}a${nChar} abbrs
3579 incr seek ${nChar}
3580 set abbrList [split $abbrs \0]
3581 set i 0
3582 set abbrevs {}
3583 foreach a $abbrList {
3584 dict set abbrevs $i $a
3585 incr i [expr { [string length $a] + 1 }]
3588 # Package up a list of tuples, each of which contains transition time,
3589 # seconds east of Greenwich, DST flag and time zone abbreviation.
3591 set r {}
3592 set lastTime $MINWIDE
3593 foreach t $times c $codes {
3594 if { $t < $lastTime } {
3595 return -code error "$fileName has times out of order"
3597 set lastTime $t
3598 lassign [lindex $types $c] gmtoff isDst abbrInd
3599 set abbrev [dict get $abbrevs $abbrInd]
3600 lappend r [list $t $gmtoff $isDst $abbrev]
3603 # In a version 2 file, there is also a POSIX-style time zone description
3604 # at the very end of the file. To get to it, skip over
3605 # nLeap leap second values (8 bytes each),
3606 # nIsStd standard/DST indicators and nIsGMT UTC/local indicators.
3608 if {$version eq {2}} {
3609 set seek [expr {$seek + 8 * $nLeap + $nIsStd + $nIsGMT + 1}]
3610 set last [string first \n $d $seek]
3611 set posix [string range $d $seek [expr {$last-1}]]
3612 if {[llength $posix] > 0} {
3613 set posixFields [ParsePosixTimeZone $posix]
3614 foreach tuple [ProcessPosixTimeZone $posixFields] {
3615 lassign $tuple t gmtoff isDst abbrev
3616 if {$t > $lastTime} {
3617 lappend r $tuple
3623 set TZData(:$fileName) $r
3625 return
3628 #----------------------------------------------------------------------
3630 # ParsePosixTimeZone --
3632 # Parses the TZ environment variable in Posix form
3634 # Parameters:
3635 # tz Time zone specifier to be interpreted
3637 # Results:
3638 # Returns a dictionary whose values contain the various pieces of
3639 # the time zone specification.
3641 # Side effects:
3642 # None.
3644 # Errors:
3645 # Throws an error if the syntax of the time zone is incorrect.
3647 # The following keys are present in the dictionary:
3648 # stdName - Name of the time zone when Daylight Saving Time
3649 # is not in effect.
3650 # stdSignum - Sign (+, -, or empty) of the offset from Greenwich
3651 # to the given (non-DST) time zone. + and the empty
3652 # string denote zones west of Greenwich, - denotes east
3653 # of Greenwich; this is contrary to the ISO convention
3654 # but follows Posix.
3655 # stdHours - Hours part of the offset from Greenwich to the given
3656 # (non-DST) time zone.
3657 # stdMinutes - Minutes part of the offset from Greenwich to the
3658 # given (non-DST) time zone. Empty denotes zero.
3659 # stdSeconds - Seconds part of the offset from Greenwich to the
3660 # given (non-DST) time zone. Empty denotes zero.
3661 # dstName - Name of the time zone when DST is in effect, or the
3662 # empty string if the time zone does not observe Daylight
3663 # Saving Time.
3664 # dstSignum, dstHours, dstMinutes, dstSeconds -
3665 # Fields corresponding to stdSignum, stdHours, stdMinutes,
3666 # stdSeconds for the Daylight Saving Time version of the
3667 # time zone. If dstHours is empty, it is presumed to be 1.
3668 # startDayOfYear - The ordinal number of the day of the year on which
3669 # Daylight Saving Time begins. If this field is
3670 # empty, then DST begins on a given month-week-day,
3671 # as below.
3672 # startJ - The letter J, or an empty string. If a J is present in
3673 # this field, then startDayOfYear does not count February 29
3674 # even in leap years.
3675 # startMonth - The number of the month in which Daylight Saving Time
3676 # begins, supplied if startDayOfYear is empty. If both
3677 # startDayOfYear and startMonth are empty, then US rules
3678 # are presumed.
3679 # startWeekOfMonth - The number of the week in the month in which
3680 # Daylight Saving Time begins, in the range 1-5.
3681 # 5 denotes the last week of the month even in a
3682 # 4-week month.
3683 # startDayOfWeek - The number of the day of the week (Sunday=0,
3684 # Saturday=6) on which Daylight Saving Time begins.
3685 # startHours - The hours part of the time of day at which Daylight
3686 # Saving Time begins. An empty string is presumed to be 2.
3687 # startMinutes - The minutes part of the time of day at which DST begins.
3688 # An empty string is presumed zero.
3689 # startSeconds - The seconds part of the time of day at which DST begins.
3690 # An empty string is presumed zero.
3691 # endDayOfYear, endJ, endMonth, endWeekOfMonth, endDayOfWeek,
3692 # endHours, endMinutes, endSeconds -
3693 # Specify the end of DST in the same way that the start* fields
3694 # specify the beginning of DST.
3696 # This procedure serves only to break the time specifier into fields.
3697 # No attempt is made to canonicalize the fields or supply default values.
3699 #----------------------------------------------------------------------
3701 proc ::tcl::clock::ParsePosixTimeZone { tz } {
3703 if {[regexp -expanded -nocase -- {
3705 # 1 - Standard time zone name
3706 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3707 # 2 - Standard time zone offset, signum
3708 ([-+]?)
3709 # 3 - Standard time zone offset, hours
3710 ([[:digit:]]{1,2})
3712 # 4 - Standard time zone offset, minutes
3713 : ([[:digit:]]{1,2})
3714 (?:
3715 # 5 - Standard time zone offset, seconds
3716 : ([[:digit:]]{1,2} )
3720 # 6 - DST time zone name
3721 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3724 # 7 - DST time zone offset, signum
3725 ([-+]?)
3726 # 8 - DST time zone offset, hours
3727 ([[:digit:]]{1,2})
3729 # 9 - DST time zone offset, minutes
3730 : ([[:digit:]]{1,2})
3731 (?:
3732 # 10 - DST time zone offset, seconds
3733 : ([[:digit:]]{1,2})
3740 # 11 - Optional J in n and Jn form 12 - Day of year
3741 ( J ? ) ( [[:digit:]]+ )
3743 # 13 - Month number 14 - Week of month 15 - Day of week
3744 ( [[:digit:]] + )
3745 [.] ( [[:digit:]] + )
3746 [.] ( [[:digit:]] + )
3749 # 16 - Start time of DST - hours
3750 / ( [[:digit:]]{1,2} )
3752 # 17 - Start time of DST - minutes
3753 : ( [[:digit:]]{1,2} )
3755 # 18 - Start time of DST - seconds
3756 : ( [[:digit:]]{1,2} )
3762 # 19 - Optional J in n and Jn form 20 - Day of year
3763 ( J ? ) ( [[:digit:]]+ )
3765 # 21 - Month number 22 - Week of month 23 - Day of week
3766 ( [[:digit:]] + )
3767 [.] ( [[:digit:]] + )
3768 [.] ( [[:digit:]] + )
3771 # 24 - End time of DST - hours
3772 / ( [[:digit:]]{1,2} )
3774 # 25 - End time of DST - minutes
3775 : ( [[:digit:]]{1,2} )
3777 # 26 - End time of DST - seconds
3778 : ( [[:digit:]]{1,2} )
3786 } $tz -> x(stdName) x(stdSignum) x(stdHours) x(stdMinutes) x(stdSeconds) \
3787 x(dstName) x(dstSignum) x(dstHours) x(dstMinutes) x(dstSeconds) \
3788 x(startJ) x(startDayOfYear) \
3789 x(startMonth) x(startWeekOfMonth) x(startDayOfWeek) \
3790 x(startHours) x(startMinutes) x(startSeconds) \
3791 x(endJ) x(endDayOfYear) \
3792 x(endMonth) x(endWeekOfMonth) x(endDayOfWeek) \
3793 x(endHours) x(endMinutes) x(endSeconds)] } {
3795 # it's a good timezone
3797 return [array get x]
3799 } else {
3801 return -code error\
3802 -errorcode [list CLOCK badTimeZone $tz] \
3803 "unable to parse time zone specification \"$tz\""
3809 #----------------------------------------------------------------------
3811 # ProcessPosixTimeZone --
3813 # Handle a Posix time zone after it's been broken out into
3814 # fields.
3816 # Parameters:
3817 # z - Dictionary returned from 'ParsePosixTimeZone'
3819 # Results:
3820 # Returns time zone information for the 'TZData' array.
3822 # Side effects:
3823 # None.
3825 #----------------------------------------------------------------------
3827 proc ::tcl::clock::ProcessPosixTimeZone { z } {
3829 variable MINWIDE
3830 variable TZData
3832 # Determine the standard time zone name and seconds east of Greenwich
3834 set stdName [dict get $z stdName]
3835 if { [string index $stdName 0] eq {<} } {
3836 set stdName [string range $stdName 1 end-1]
3838 if { [dict get $z stdSignum] eq {-} } {
3839 set stdSignum +1
3840 } else {
3841 set stdSignum -1
3843 set stdHours [lindex [::scan [dict get $z stdHours] %d] 0]
3844 if { [dict get $z stdMinutes] ne {} } {
3845 set stdMinutes [lindex [::scan [dict get $z stdMinutes] %d] 0]
3846 } else {
3847 set stdMinutes 0
3849 if { [dict get $z stdSeconds] ne {} } {
3850 set stdSeconds [lindex [::scan [dict get $z stdSeconds] %d] 0]
3851 } else {
3852 set stdSeconds 0
3854 set stdOffset [expr { ( ( $stdHours * 60 + $stdMinutes )
3855 * 60 + $stdSeconds )
3856 * $stdSignum }]
3857 set data [list [list $MINWIDE $stdOffset 0 $stdName]]
3859 # If there's no daylight zone, we're done
3861 set dstName [dict get $z dstName]
3862 if { $dstName eq {} } {
3863 return $data
3865 if { [string index $dstName 0] eq {<} } {
3866 set dstName [string range $dstName 1 end-1]
3869 # Determine the daylight name
3871 if { [dict get $z dstSignum] eq {-} } {
3872 set dstSignum +1
3873 } else {
3874 set dstSignum -1
3876 if { [dict get $z dstHours] eq {} } {
3877 set dstOffset [expr { 3600 + $stdOffset }]
3878 } else {
3879 set dstHours [lindex [::scan [dict get $z dstHours] %d] 0]
3880 if { [dict get $z dstMinutes] ne {} } {
3881 set dstMinutes [lindex [::scan [dict get $z dstMinutes] %d] 0]
3882 } else {
3883 set dstMinutes 0
3885 if { [dict get $z dstSeconds] ne {} } {
3886 set dstSeconds [lindex [::scan [dict get $z dstSeconds] %d] 0]
3887 } else {
3888 set dstSeconds 0
3890 set dstOffset [expr { ( ( $dstHours * 60 + $dstMinutes )
3891 * 60 + $dstSeconds )
3892 * $dstSignum }]
3895 # Fill in defaults for European or US DST rules
3896 # US start time is the second Sunday in March
3897 # EU start time is the last Sunday in March
3898 # US end time is the first Sunday in November.
3899 # EU end time is the last Sunday in October
3901 if { [dict get $z startDayOfYear] eq {}
3902 && [dict get $z startMonth] eq {} } {
3903 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3904 # EU
3905 dict set z startWeekOfMonth 5
3906 if {$stdHours>2} {
3907 dict set z startHours 2
3908 } else {
3909 dict set z startHours [expr {$stdHours+1}]
3911 } else {
3912 # US
3913 dict set z startWeekOfMonth 2
3914 dict set z startHours 2
3916 dict set z startMonth 3
3917 dict set z startDayOfWeek 0
3918 dict set z startMinutes 0
3919 dict set z startSeconds 0
3921 if { [dict get $z endDayOfYear] eq {}
3922 && [dict get $z endMonth] eq {} } {
3923 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3924 # EU
3925 dict set z endMonth 10
3926 dict set z endWeekOfMonth 5
3927 if {$stdHours>2} {
3928 dict set z endHours 3
3929 } else {
3930 dict set z endHours [expr {$stdHours+2}]
3932 } else {
3933 # US
3934 dict set z endMonth 11
3935 dict set z endWeekOfMonth 1
3936 dict set z endHours 2
3938 dict set z endDayOfWeek 0
3939 dict set z endMinutes 0
3940 dict set z endSeconds 0
3943 # Put DST in effect in all years from 1916 to 2099.
3945 for { set y 1916 } { $y < 2099 } { incr y } {
3946 set startTime [DeterminePosixDSTTime $z start $y]
3947 incr startTime [expr { - wide($stdOffset) }]
3948 set endTime [DeterminePosixDSTTime $z end $y]
3949 incr endTime [expr { - wide($dstOffset) }]
3950 if { $startTime < $endTime } {
3951 lappend data \
3952 [list $startTime $dstOffset 1 $dstName] \
3953 [list $endTime $stdOffset 0 $stdName]
3954 } else {
3955 lappend data \
3956 [list $endTime $stdOffset 0 $stdName] \
3957 [list $startTime $dstOffset 1 $dstName]
3961 return $data
3965 #----------------------------------------------------------------------
3967 # DeterminePosixDSTTime --
3969 # Determines the time that Daylight Saving Time starts or ends
3970 # from a Posix time zone specification.
3972 # Parameters:
3973 # z - Time zone data returned from ParsePosixTimeZone.
3974 # Missing fields are expected to be filled in with
3975 # default values.
3976 # bound - The word 'start' or 'end'
3977 # y - The year for which the transition time is to be determined.
3979 # Results:
3980 # Returns the transition time as a count of seconds from
3981 # the epoch. The time is relative to the wall clock, not UTC.
3983 #----------------------------------------------------------------------
3985 proc ::tcl::clock::DeterminePosixDSTTime { z bound y } {
3987 variable FEB_28
3989 # Determine the start or end day of DST
3991 set date [dict create era CE year $y]
3992 set doy [dict get $z ${bound}DayOfYear]
3993 if { $doy ne {} } {
3995 # Time was specified as a day of the year
3997 if { [dict get $z ${bound}J] ne {}
3998 && [IsGregorianLeapYear $y]
3999 && ( $doy > $FEB_28 ) } {
4000 incr doy
4002 dict set date dayOfYear $doy
4003 set date [GetJulianDayFromEraYearDay $date[set date {}] 2361222]
4004 } else {
4006 # Time was specified as a day of the week within a month
4008 dict set date month [dict get $z ${bound}Month]
4009 dict set date dayOfWeek [dict get $z ${bound}DayOfWeek]
4010 set dowim [dict get $z ${bound}WeekOfMonth]
4011 if { $dowim >= 5 } {
4012 set dowim -1
4014 dict set date dayOfWeekInMonth $dowim
4015 set date [GetJulianDayFromEraYearMonthWeekDay $date[set date {}] 2361222]
4019 set jd [dict get $date julianDay]
4020 set seconds [expr { wide($jd) * wide(86400)
4021 - wide(210866803200) }]
4023 set h [dict get $z ${bound}Hours]
4024 if { $h eq {} } {
4025 set h 2
4026 } else {
4027 set h [lindex [::scan $h %d] 0]
4029 set m [dict get $z ${bound}Minutes]
4030 if { $m eq {} } {
4031 set m 0
4032 } else {
4033 set m [lindex [::scan $m %d] 0]
4035 set s [dict get $z ${bound}Seconds]
4036 if { $s eq {} } {
4037 set s 0
4038 } else {
4039 set s [lindex [::scan $s %d] 0]
4041 set tod [expr { ( $h * 60 + $m ) * 60 + $s }]
4042 return [expr { $seconds + $tod }]
4046 #----------------------------------------------------------------------
4048 # GetLocaleEra --
4050 # Given local time expressed in seconds from the Posix epoch,
4051 # determine localized era and year within the era.
4053 # Parameters:
4054 # date - Dictionary that must contain the keys, 'localSeconds',
4055 # whose value is expressed as the appropriate local time;
4056 # and 'year', whose value is the Gregorian year.
4057 # etable - Value of the LOCALE_ERAS key in the message catalogue
4058 # for the target locale.
4060 # Results:
4061 # Returns the dictionary, augmented with the keys, 'localeEra'
4062 # and 'localeYear'.
4064 #----------------------------------------------------------------------
4066 proc ::tcl::clock::GetLocaleEra { date etable } {
4068 set index [BSearch $etable [dict get $date localSeconds]]
4069 if { $index < 0} {
4070 dict set date localeEra \
4071 [::format %02d [expr { [dict get $date year] / 100 }]]
4072 dict set date localeYear \
4073 [expr { [dict get $date year] % 100 }]
4074 } else {
4075 dict set date localeEra [lindex $etable $index 1]
4076 dict set date localeYear [expr { [dict get $date year]
4077 - [lindex $etable $index 2] }]
4079 return $date
4083 #----------------------------------------------------------------------
4085 # GetJulianDayFromEraYearDay --
4087 # Given a year, month and day on the Gregorian calendar, determines
4088 # the Julian Day Number beginning at noon on that date.
4090 # Parameters:
4091 # date -- A dictionary in which the 'era', 'year', and
4092 # 'dayOfYear' slots are populated. The calendar in use
4093 # is determined by the date itself relative to:
4094 # changeover -- Julian day on which the Gregorian calendar was
4095 # adopted in the current locale.
4097 # Results:
4098 # Returns the given dictionary augmented with a 'julianDay' key
4099 # whose value is the desired Julian Day Number, and a 'gregorian'
4100 # key that specifies whether the calendar is Gregorian (1) or
4101 # Julian (0).
4103 # Side effects:
4104 # None.
4106 # Bugs:
4107 # This code needs to be moved to the C layer.
4109 #----------------------------------------------------------------------
4111 proc ::tcl::clock::GetJulianDayFromEraYearDay {date changeover} {
4113 # Get absolute year number from the civil year
4115 switch -exact -- [dict get $date era] {
4116 BCE {
4117 set year [expr { 1 - [dict get $date year] }]
4119 CE {
4120 set year [dict get $date year]
4123 set ym1 [expr { $year - 1 }]
4125 # Try the Gregorian calendar first.
4127 dict set date gregorian 1
4128 set jd [expr { 1721425
4129 + [dict get $date dayOfYear]
4130 + ( 365 * $ym1 )
4131 + ( $ym1 / 4 )
4132 - ( $ym1 / 100 )
4133 + ( $ym1 / 400 ) }]
4135 # If the date is before the Gregorian change, use the Julian calendar.
4137 if { $jd < $changeover } {
4138 dict set date gregorian 0
4139 set jd [expr { 1721423
4140 + [dict get $date dayOfYear]
4141 + ( 365 * $ym1 )
4142 + ( $ym1 / 4 ) }]
4145 dict set date julianDay $jd
4146 return $date
4149 #----------------------------------------------------------------------
4151 # GetJulianDayFromEraYearMonthWeekDay --
4153 # Determines the Julian Day number corresponding to the nth
4154 # given day-of-the-week in a given month.
4156 # Parameters:
4157 # date - Dictionary containing the keys, 'era', 'year', 'month'
4158 # 'weekOfMonth', 'dayOfWeek', and 'dayOfWeekInMonth'.
4159 # changeover - Julian Day of adoption of the Gregorian calendar
4161 # Results:
4162 # Returns the given dictionary, augmented with a 'julianDay' key.
4164 # Side effects:
4165 # None.
4167 # Bugs:
4168 # This code needs to be moved to the C layer.
4170 #----------------------------------------------------------------------
4172 proc ::tcl::clock::GetJulianDayFromEraYearMonthWeekDay {date changeover} {
4174 # Come up with a reference day; either the zeroeth day of the
4175 # given month (dayOfWeekInMonth >= 0) or the seventh day of the
4176 # following month (dayOfWeekInMonth < 0)
4178 set date2 $date
4179 set week [dict get $date dayOfWeekInMonth]
4180 if { $week >= 0 } {
4181 dict set date2 dayOfMonth 0
4182 } else {
4183 dict incr date2 month
4184 dict set date2 dayOfMonth 7
4186 set date2 [GetJulianDayFromEraYearMonthDay $date2[set date2 {}] \
4187 $changeover]
4188 set wd0 [WeekdayOnOrBefore [dict get $date dayOfWeek] \
4189 [dict get $date2 julianDay]]
4190 dict set date julianDay [expr { $wd0 + 7 * $week }]
4191 return $date
4195 #----------------------------------------------------------------------
4197 # IsGregorianLeapYear --
4199 # Determines whether a given date represents a leap year in the
4200 # Gregorian calendar.
4202 # Parameters:
4203 # date -- The date to test. The fields, 'era', 'year' and 'gregorian'
4204 # must be set.
4206 # Results:
4207 # Returns 1 if the year is a leap year, 0 otherwise.
4209 # Side effects:
4210 # None.
4212 #----------------------------------------------------------------------
4214 proc ::tcl::clock::IsGregorianLeapYear { date } {
4216 switch -exact -- [dict get $date era] {
4217 BCE {
4218 set year [expr { 1 - [dict get $date year]}]
4220 CE {
4221 set year [dict get $date year]
4224 if { $year % 4 != 0 } {
4225 return 0
4226 } elseif { ![dict get $date gregorian] } {
4227 return 1
4228 } elseif { $year % 400 == 0 } {
4229 return 1
4230 } elseif { $year % 100 == 0 } {
4231 return 0
4232 } else {
4233 return 1
4238 #----------------------------------------------------------------------
4240 # WeekdayOnOrBefore --
4242 # Determine the nearest day of week (given by the 'weekday'
4243 # parameter, Sunday==0) on or before a given Julian Day.
4245 # Parameters:
4246 # weekday -- Day of the week
4247 # j -- Julian Day number
4249 # Results:
4250 # Returns the Julian Day Number of the desired date.
4252 # Side effects:
4253 # None.
4255 #----------------------------------------------------------------------
4257 proc ::tcl::clock::WeekdayOnOrBefore { weekday j } {
4259 set k [expr { ( $weekday + 6 ) % 7 }]
4260 return [expr { $j - ( $j - $k ) % 7 }]
4264 #----------------------------------------------------------------------
4266 # BSearch --
4268 # Service procedure that does binary search in several places
4269 # inside the 'clock' command.
4271 # Parameters:
4272 # list - List of lists, sorted in ascending order by the
4273 # first elements
4274 # key - Value to search for
4276 # Results:
4277 # Returns the index of the greatest element in $list that is less
4278 # than or equal to $key.
4280 # Side effects:
4281 # None.
4283 #----------------------------------------------------------------------
4285 proc ::tcl::clock::BSearch { list key } {
4287 if {[llength $list] == 0} {
4288 return -1
4290 if { $key < [lindex $list 0 0] } {
4291 return -1
4294 set l 0
4295 set u [expr { [llength $list] - 1 }]
4297 while { $l < $u } {
4299 # At this point, we know that
4300 # $k >= [lindex $list $l 0]
4301 # Either $u == [llength $list] or else $k < [lindex $list $u+1 0]
4302 # We find the midpoint of the interval {l,u} rounded UP, compare
4303 # against it, and set l or u to maintain the invariant. Note
4304 # that the interval shrinks at each step, guaranteeing convergence.
4306 set m [expr { ( $l + $u + 1 ) / 2 }]
4307 if { $key >= [lindex $list $m 0] } {
4308 set l $m
4309 } else {
4310 set u [expr { $m - 1 }]
4314 return $l
4317 #----------------------------------------------------------------------
4319 # clock add --
4321 # Adds an offset to a given time.
4323 # Syntax:
4324 # clock add clockval ?count unit?... ?-option value?
4326 # Parameters:
4327 # clockval -- Starting time value
4328 # count -- Amount of a unit of time to add
4329 # unit -- Unit of time to add, must be one of:
4330 # years year months month weeks week
4331 # days day hours hour minutes minute
4332 # seconds second
4334 # Options:
4335 # -gmt BOOLEAN
4336 # (Deprecated) Flag synonymous with '-timezone :GMT'
4337 # -timezone ZONE
4338 # Name of the time zone in which calculations are to be done.
4339 # -locale NAME
4340 # Name of the locale in which calculations are to be done.
4341 # Used to determine the Gregorian change date.
4343 # Results:
4344 # Returns the given time adjusted by the given offset(s) in
4345 # order.
4347 # Notes:
4348 # It is possible that adding a number of months or years will adjust
4349 # the day of the month as well. For instance, the time at
4350 # one month after 31 January is either 28 or 29 February, because
4351 # February has fewer than 31 days.
4353 #----------------------------------------------------------------------
4355 proc ::tcl::clock::add { clockval args } {
4357 if { [llength $args] % 2 != 0 } {
4358 set cmdName "clock add"
4359 return -code error \
4360 -errorcode [list CLOCK wrongNumArgs] \
4361 "wrong \# args: should be\
4362 \"$cmdName clockval ?number units?...\
4363 ?-gmt boolean? ?-locale LOCALE? ?-timezone ZONE?\""
4365 if { [catch { expr {wide($clockval)} } result] } {
4366 return -code error $result
4369 set offsets {}
4370 set gmt 0
4371 set locale c
4372 set timezone [GetSystemTimeZone]
4374 foreach { a b } $args {
4376 if { [string is integer -strict $a] } {
4378 lappend offsets $a $b
4380 } else {
4382 switch -exact -- $a {
4384 -g - -gm - -gmt {
4385 set gmt $b
4387 -l - -lo - -loc - -loca - -local - -locale {
4388 set locale [string tolower $b]
4390 -t - -ti - -tim - -time - -timez - -timezo - -timezon -
4391 -timezone {
4392 set timezone $b
4394 default {
4395 return -code error \
4396 -errorcode [list CLOCK badSwitch $a] \
4397 "bad switch \"$a\",\
4398 must be -gmt, -locale or -timezone"
4404 # Check options for validity
4406 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
4407 return -code error \
4408 -errorcode [list CLOCK gmtWithTimezone] \
4409 "cannot use -gmt and -timezone in same call"
4411 if { [catch { expr { wide($clockval) } } result] } {
4412 return -code error \
4413 "expected integer but got \"$clockval\""
4415 if { ![string is boolean $gmt] } {
4416 return -code error \
4417 "expected boolean value but got \"$gmt\""
4418 } else {
4419 if { $gmt } {
4420 set timezone :GMT
4424 EnterLocale $locale oldLocale
4426 set changeover [mc GREGORIAN_CHANGE_DATE]
4428 if {[catch {SetupTimeZone $timezone} retval opts]} {
4429 dict unset opts -errorinfo
4430 return -options $opts $retval
4433 set status [catch {
4435 foreach { quantity unit } $offsets {
4437 switch -exact -- $unit {
4439 years - year {
4440 set clockval \
4441 [AddMonths [expr { 12 * $quantity }] \
4442 $clockval $timezone $changeover]
4444 months - month {
4445 set clockval [AddMonths $quantity $clockval $timezone \
4446 $changeover]
4449 weeks - week {
4450 set clockval [AddDays [expr { 7 * $quantity }] \
4451 $clockval $timezone $changeover]
4453 days - day {
4454 set clockval [AddDays $quantity $clockval $timezone \
4455 $changeover]
4458 hours - hour {
4459 set clockval [expr { 3600 * $quantity + $clockval }]
4461 minutes - minute {
4462 set clockval [expr { 60 * $quantity + $clockval }]
4464 seconds - second {
4465 set clockval [expr { $quantity + $clockval }]
4468 default {
4469 error "unknown unit \"$unit\", must be \
4470 years, months, weeks, days, hours, minutes or seconds" \
4471 "unknown unit \"$unit\", must be \
4472 years, months, weeks, days, hours, minutes or seconds" \
4473 [list CLOCK badUnit $unit]
4477 } result opts]
4479 # Restore the locale
4481 if { [info exists oldLocale] } {
4482 mclocale $oldLocale
4485 if { $status == 1 } {
4486 if { [lindex [dict get $opts -errorcode] 0] eq {CLOCK} } {
4487 dict unset opts -errorinfo
4489 return -options $opts $result
4490 } else {
4491 return $clockval
4496 #----------------------------------------------------------------------
4498 # AddMonths --
4500 # Add a given number of months to a given clock value in a given
4501 # time zone.
4503 # Parameters:
4504 # months - Number of months to add (may be negative)
4505 # clockval - Seconds since the epoch before the operation
4506 # timezone - Time zone in which the operation is to be performed
4508 # Results:
4509 # Returns the new clock value as a number of seconds since
4510 # the epoch.
4512 # Side effects:
4513 # None.
4515 #----------------------------------------------------------------------
4517 proc ::tcl::clock::AddMonths { months clockval timezone changeover } {
4519 variable DaysInRomanMonthInCommonYear
4520 variable DaysInRomanMonthInLeapYear
4521 variable TZData
4523 # Convert the time to year, month, day, and fraction of day.
4525 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4526 dict set date secondOfDay [expr { [dict get $date localSeconds]
4527 % 86400 }]
4528 dict set date tzName $timezone
4530 # Add the requisite number of months
4532 set m [dict get $date month]
4533 incr m $months
4534 incr m -1
4535 set delta [expr { $m / 12 }]
4536 set mm [expr { $m % 12 }]
4537 dict set date month [expr { $mm + 1 }]
4538 dict incr date year $delta
4540 # If the date doesn't exist in the current month, repair it
4542 if { [IsGregorianLeapYear $date] } {
4543 set hath [lindex $DaysInRomanMonthInLeapYear $mm]
4544 } else {
4545 set hath [lindex $DaysInRomanMonthInCommonYear $mm]
4547 if { [dict get $date dayOfMonth] > $hath } {
4548 dict set date dayOfMonth $hath
4551 # Reconvert to a number of seconds
4553 set date [GetJulianDayFromEraYearMonthDay \
4554 $date[set date {}]\
4555 $changeover]
4556 dict set date localSeconds \
4557 [expr { -210866803200
4558 + ( 86400 * wide([dict get $date julianDay]) )
4559 + [dict get $date secondOfDay] }]
4560 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4561 $changeover]
4563 return [dict get $date seconds]
4567 #----------------------------------------------------------------------
4569 # AddDays --
4571 # Add a given number of days to a given clock value in a given
4572 # time zone.
4574 # Parameters:
4575 # days - Number of days to add (may be negative)
4576 # clockval - Seconds since the epoch before the operation
4577 # timezone - Time zone in which the operation is to be performed
4578 # changeover - Julian Day on which the Gregorian calendar was adopted
4579 # in the target locale.
4581 # Results:
4582 # Returns the new clock value as a number of seconds since
4583 # the epoch.
4585 # Side effects:
4586 # None.
4588 #----------------------------------------------------------------------
4590 proc ::tcl::clock::AddDays { days clockval timezone changeover } {
4592 variable TZData
4594 # Convert the time to Julian Day
4596 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4597 dict set date secondOfDay [expr { [dict get $date localSeconds]
4598 % 86400 }]
4599 dict set date tzName $timezone
4601 # Add the requisite number of days
4603 dict incr date julianDay $days
4605 # Reconvert to a number of seconds
4607 dict set date localSeconds \
4608 [expr { -210866803200
4609 + ( 86400 * wide([dict get $date julianDay]) )
4610 + [dict get $date secondOfDay] }]
4611 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4612 $changeover]
4614 return [dict get $date seconds]
4618 #----------------------------------------------------------------------
4620 # mc --
4622 # Wrapper around ::msgcat::mc that caches the result according
4623 # to the locale.
4625 # Parameters:
4626 # Accepts the name of the message to retrieve.
4628 # Results:
4629 # Returns the message text.
4631 # Side effects:
4632 # Caches the message text.
4634 # Notes:
4635 # Only the single-argument version of [mc] is supported.
4637 #----------------------------------------------------------------------
4639 proc ::tcl::clock::mc { name } {
4640 variable McLoaded
4641 set Locale [mclocale]
4642 if { [dict exists $McLoaded $Locale $name] } {
4643 return [dict get $McLoaded $Locale $name]
4644 } else {
4645 set val [::msgcat::mc $name]
4646 dict set McLoaded $Locale $name $val
4647 return $val
4651 #----------------------------------------------------------------------
4653 # ClearCaches --
4655 # Clears all caches to reclaim the memory used in [clock]
4657 # Parameters:
4658 # None.
4660 # Results:
4661 # None.
4663 # Side effects:
4664 # Caches are cleared.
4666 #----------------------------------------------------------------------
4668 proc ::tcl::clock::ClearCaches {} {
4670 variable FormatProc
4671 variable LocaleNumeralCache
4672 variable McLoaded
4673 variable CachedSystemTimeZone
4674 variable TimeZoneBad
4676 foreach p [info procs [namespace current]::scanproc'*] {
4677 rename $p {}
4679 foreach p [info procs [namespace current]::formatproc'*] {
4680 rename $p {}
4683 catch {unset FormatProc}
4684 set LocaleNumeralCache {}
4685 set McLoaded {}
4686 catch {unset CachedSystemTimeZone}
4687 set TimeZoneBad {}
4688 InitTZData