Update tcl to version 8.5.13
[msysgit.git] / mingw / lib / tcl8.5 / clock.tcl
blob32911b3a242b97d11c44ee00dc6392f6d6a69973
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 #----------------------------------------------------------------------
18 # We must have message catalogs that support the root locale, and
19 # we need access to the Registry on Windows systems.
21 uplevel \#0 {
22 package require msgcat 1.4
23 if { $::tcl_platform(platform) eq {windows} } {
24 if { [catch { package require registry 1.1 }] } {
25 namespace eval ::tcl::clock [list variable NoRegistry {}]
30 # Put the library directory into the namespace for the ensemble
31 # so that the library code can find message catalogs and time zone
32 # definition files.
34 namespace eval ::tcl::clock \
35 [list variable LibDir [file dirname [info script]]]
37 #----------------------------------------------------------------------
39 # clock --
41 # Manipulate times.
43 # The 'clock' command manipulates time. Refer to the user documentation
44 # for the available subcommands and what they do.
46 #----------------------------------------------------------------------
48 namespace eval ::tcl::clock {
50 # Export the subcommands
52 namespace export format
53 namespace export clicks
54 namespace export microseconds
55 namespace export milliseconds
56 namespace export scan
57 namespace export seconds
58 namespace export add
60 # Import the message catalog commands that we use.
62 namespace import ::msgcat::mcload
63 namespace import ::msgcat::mclocale
67 #----------------------------------------------------------------------
69 # ::tcl::clock::Initialize --
71 # Finish initializing the 'clock' subsystem
73 # Results:
74 # None.
76 # Side effects:
77 # Namespace variable in the 'clock' subsystem are initialized.
79 # The '::tcl::clock::Initialize' procedure initializes the namespace
80 # variables and root locale message catalog for the 'clock' subsystem.
81 # It is broken into a procedure rather than simply evaluated as a script
82 # so that it will be able to use local variables, avoiding the dangers
83 # of 'creative writing' as in Bug 1185933.
85 #----------------------------------------------------------------------
87 proc ::tcl::clock::Initialize {} {
89 rename ::tcl::clock::Initialize {}
91 variable LibDir
93 # Define the Greenwich time zone
95 proc InitTZData {} {
96 variable TZData
97 array unset TZData
98 set TZData(:Etc/GMT) {
99 {-9223372036854775808 0 0 GMT}
101 set TZData(:GMT) $TZData(:Etc/GMT)
102 set TZData(:Etc/UTC) {
103 {-9223372036854775808 0 0 UTC}
105 set TZData(:UTC) $TZData(:Etc/UTC)
106 set TZData(:localtime) {}
108 InitTZData
110 # Define the message catalog for the root locale.
112 ::msgcat::mcmset {} {
113 AM {am}
114 BCE {B.C.E.}
115 CE {C.E.}
116 DATE_FORMAT {%m/%d/%Y}
117 DATE_TIME_FORMAT {%a %b %e %H:%M:%S %Y}
118 DAYS_OF_WEEK_ABBREV {
119 Sun Mon Tue Wed Thu Fri Sat
121 DAYS_OF_WEEK_FULL {
122 Sunday Monday Tuesday Wednesday Thursday Friday Saturday
124 GREGORIAN_CHANGE_DATE 2299161
125 LOCALE_DATE_FORMAT {%m/%d/%Y}
126 LOCALE_DATE_TIME_FORMAT {%a %b %e %H:%M:%S %Y}
127 LOCALE_ERAS {}
128 LOCALE_NUMERALS {
129 00 01 02 03 04 05 06 07 08 09
130 10 11 12 13 14 15 16 17 18 19
131 20 21 22 23 24 25 26 27 28 29
132 30 31 32 33 34 35 36 37 38 39
133 40 41 42 43 44 45 46 47 48 49
134 50 51 52 53 54 55 56 57 58 59
135 60 61 62 63 64 65 66 67 68 69
136 70 71 72 73 74 75 76 77 78 79
137 80 81 82 83 84 85 86 87 88 89
138 90 91 92 93 94 95 96 97 98 99
140 LOCALE_TIME_FORMAT {%H:%M:%S}
141 LOCALE_YEAR_FORMAT {%EC%Ey}
142 MONTHS_ABBREV {
143 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
145 MONTHS_FULL {
146 January February March
147 April May June
148 July August September
149 October November December
151 PM {pm}
152 TIME_FORMAT {%H:%M:%S}
153 TIME_FORMAT_12 {%I:%M:%S %P}
154 TIME_FORMAT_24 {%H:%M}
155 TIME_FORMAT_24_SECS {%H:%M:%S}
158 # Define a few Gregorian change dates for other locales. In most cases
159 # the change date follows a language, because a nation's colonies changed
160 # at the same time as the nation itself. In many cases, different
161 # national boundaries existed; the dominating rule is to follow the
162 # nation's capital.
164 # Italy, Spain, Portugal, Poland
166 ::msgcat::mcset it GREGORIAN_CHANGE_DATE 2299161
167 ::msgcat::mcset es GREGORIAN_CHANGE_DATE 2299161
168 ::msgcat::mcset pt GREGORIAN_CHANGE_DATE 2299161
169 ::msgcat::mcset pl GREGORIAN_CHANGE_DATE 2299161
171 # France, Austria
173 ::msgcat::mcset fr GREGORIAN_CHANGE_DATE 2299227
175 # For Belgium, we follow Southern Netherlands; Liege Diocese
176 # changed several weeks later.
178 ::msgcat::mcset fr_BE GREGORIAN_CHANGE_DATE 2299238
179 ::msgcat::mcset nl_BE GREGORIAN_CHANGE_DATE 2299238
181 # Austria
183 ::msgcat::mcset de_AT GREGORIAN_CHANGE_DATE 2299527
185 # Hungary
187 ::msgcat::mcset hu GREGORIAN_CHANGE_DATE 2301004
189 # Germany, Norway, Denmark (Catholic Germany changed earlier)
191 ::msgcat::mcset de_DE GREGORIAN_CHANGE_DATE 2342032
192 ::msgcat::mcset nb GREGORIAN_CHANGE_DATE 2342032
193 ::msgcat::mcset nn GREGORIAN_CHANGE_DATE 2342032
194 ::msgcat::mcset no GREGORIAN_CHANGE_DATE 2342032
195 ::msgcat::mcset da GREGORIAN_CHANGE_DATE 2342032
197 # Holland (Brabant, Gelderland, Flanders, Friesland, etc. changed
198 # at various times)
200 ::msgcat::mcset nl GREGORIAN_CHANGE_DATE 2342165
202 # Protestant Switzerland (Catholic cantons changed earlier)
204 ::msgcat::mcset fr_CH GREGORIAN_CHANGE_DATE 2361342
205 ::msgcat::mcset it_CH GREGORIAN_CHANGE_DATE 2361342
206 ::msgcat::mcset de_CH GREGORIAN_CHANGE_DATE 2361342
208 # English speaking countries
210 ::msgcat::mcset en GREGORIAN_CHANGE_DATE 2361222
212 # Sweden (had several changes onto and off of the Gregorian calendar)
214 ::msgcat::mcset sv GREGORIAN_CHANGE_DATE 2361390
216 # Russia
218 ::msgcat::mcset ru GREGORIAN_CHANGE_DATE 2421639
220 # Romania (Transylvania changed earler - perhaps de_RO should show
221 # the earlier date?)
223 ::msgcat::mcset ro GREGORIAN_CHANGE_DATE 2422063
225 # Greece
227 ::msgcat::mcset el GREGORIAN_CHANGE_DATE 2423480
229 #------------------------------------------------------------------
231 # CONSTANTS
233 #------------------------------------------------------------------
235 # Paths at which binary time zone data for the Olson libraries
236 # are known to reside on various operating systems
238 variable ZoneinfoPaths {}
239 foreach path {
240 /usr/share/zoneinfo
241 /usr/share/lib/zoneinfo
242 /usr/lib/zoneinfo
243 /usr/local/etc/zoneinfo
245 if { [file isdirectory $path] } {
246 lappend ZoneinfoPaths $path
250 # Define the directories for time zone data and message catalogs.
252 variable DataDir [file join $LibDir tzdata]
253 variable MsgDir [file join $LibDir msgs]
255 # Number of days in the months, in common years and leap years.
257 variable DaysInRomanMonthInCommonYear \
258 { 31 28 31 30 31 30 31 31 30 31 30 31 }
259 variable DaysInRomanMonthInLeapYear \
260 { 31 29 31 30 31 30 31 31 30 31 30 31 }
261 variable DaysInPriorMonthsInCommonYear [list 0]
262 variable DaysInPriorMonthsInLeapYear [list 0]
263 set i 0
264 foreach j $DaysInRomanMonthInCommonYear {
265 lappend DaysInPriorMonthsInCommonYear [incr i $j]
267 set i 0
268 foreach j $DaysInRomanMonthInLeapYear {
269 lappend DaysInPriorMonthsInLeapYear [incr i $j]
272 # Another epoch (Hi, Jeff!)
274 variable Roddenberry 1946
276 # Integer ranges
278 variable MINWIDE -9223372036854775808
279 variable MAXWIDE 9223372036854775807
281 # Day before Leap Day
283 variable FEB_28 58
285 # Translation table to map Windows TZI onto cities, so that
286 # the Olson rules can apply. In some cases the mapping is ambiguous,
287 # so it's wise to specify $::env(TCL_TZ) rather than simply depending
288 # on the system time zone.
290 # The keys are long lists of values obtained from the time zone
291 # information in the Registry. In order, the list elements are:
292 # Bias StandardBias DaylightBias
293 # StandardDate.wYear StandardDate.wMonth StandardDate.wDayOfWeek
294 # StandardDate.wDay StandardDate.wHour StandardDate.wMinute
295 # StandardDate.wSecond StandardDate.wMilliseconds
296 # DaylightDate.wYear DaylightDate.wMonth DaylightDate.wDayOfWeek
297 # DaylightDate.wDay DaylightDate.wHour DaylightDate.wMinute
298 # DaylightDate.wSecond DaylightDate.wMilliseconds
299 # The values are the names of time zones where those rules apply.
300 # There is considerable ambiguity in certain zones; an attempt has
301 # been made to make a reasonable guess, but this table needs to be
302 # taken with a grain of salt.
304 variable WinZoneInfo [dict create {*}{
305 {-43200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Kwajalein
306 {-39600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Midway
307 {-36000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Honolulu
308 {-32400 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Anchorage
309 {-28800 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Los_Angeles
310 {-28800 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Tijuana
311 {-25200 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Denver
312 {-25200 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Chihuahua
313 {-25200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Phoenix
314 {-21600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Regina
315 {-21600 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Chicago
316 {-21600 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Mexico_City
317 {-18000 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/New_York
318 {-18000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Indianapolis
319 {-14400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Caracas
320 {-14400 0 3600 0 3 6 2 23 59 59 999 0 10 6 2 23 59 59 999}
321 :America/Santiago
322 {-14400 0 3600 0 2 0 5 2 0 0 0 0 11 0 1 2 0 0 0} :America/Manaus
323 {-14400 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Halifax
324 {-12600 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/St_Johns
325 {-10800 0 3600 0 2 0 2 2 0 0 0 0 10 0 3 2 0 0 0} :America/Sao_Paulo
326 {-10800 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Godthab
327 {-10800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Buenos_Aires
328 {-10800 0 3600 0 2 0 5 2 0 0 0 0 11 0 1 2 0 0 0} :America/Brasilia
329 {-10800 0 3600 0 3 0 2 2 0 0 0 0 10 0 1 2 0 0 0} :America/Montevideo
330 {-7200 0 3600 0 9 0 5 2 0 0 0 0 3 0 5 2 0 0 0} :America/Noronha
331 {-3600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Atlantic/Azores
332 {-3600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Atlantic/Cape_Verde
333 {0 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :UTC
334 {0 0 3600 0 10 0 5 2 0 0 0 0 3 0 5 1 0 0 0} :Europe/London
335 {3600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Africa/Kinshasa
336 {3600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :CET
337 {7200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Africa/Harare
338 {7200 0 3600 0 9 4 5 23 59 59 0 0 4 4 5 23 59 59 0}
339 :Africa/Cairo
340 {7200 0 3600 0 10 0 5 4 0 0 0 0 3 0 5 3 0 0 0} :Europe/Helsinki
341 {7200 0 3600 0 9 0 3 2 0 0 0 0 3 5 5 2 0 0 0} :Asia/Jerusalem
342 {7200 0 3600 0 9 0 5 1 0 0 0 0 3 0 5 0 0 0 0} :Europe/Bucharest
343 {7200 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Europe/Athens
344 {7200 0 3600 0 9 5 5 1 0 0 0 0 3 4 5 0 0 0 0} :Asia/Amman
345 {7200 0 3600 0 10 6 5 23 59 59 999 0 3 0 5 0 0 0 0}
346 :Asia/Beirut
347 {7200 0 -3600 0 4 0 1 2 0 0 0 0 9 0 1 2 0 0 0} :Africa/Windhoek
348 {10800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Riyadh
349 {10800 0 3600 0 10 0 1 4 0 0 0 0 4 0 1 3 0 0 0} :Asia/Baghdad
350 {10800 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Europe/Moscow
351 {12600 0 3600 0 9 2 4 2 0 0 0 0 3 0 1 2 0 0 0} :Asia/Tehran
352 {14400 0 3600 0 10 0 5 5 0 0 0 0 3 0 5 4 0 0 0} :Asia/Baku
353 {14400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Muscat
354 {14400 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Tbilisi
355 {16200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Kabul
356 {18000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Karachi
357 {18000 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Yekaterinburg
358 {19800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Calcutta
359 {20700 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Katmandu
360 {21600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Dhaka
361 {21600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Novosibirsk
362 {23400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Rangoon
363 {25200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Bangkok
364 {25200 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Krasnoyarsk
365 {28800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Chongqing
366 {28800 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Irkutsk
367 {32400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Tokyo
368 {32400 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Yakutsk
369 {34200 0 3600 0 3 0 5 3 0 0 0 0 10 0 5 2 0 0 0} :Australia/Adelaide
370 {34200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Australia/Darwin
371 {36000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Australia/Brisbane
372 {36000 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Vladivostok
373 {36000 0 3600 0 3 0 5 3 0 0 0 0 10 0 1 2 0 0 0} :Australia/Hobart
374 {36000 0 3600 0 3 0 5 3 0 0 0 0 10 0 5 2 0 0 0} :Australia/Sydney
375 {39600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Noumea
376 {43200 0 3600 0 3 0 3 3 0 0 0 0 10 0 1 2 0 0 0} :Pacific/Auckland
377 {43200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Fiji
378 {46800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Tongatapu
381 # Groups of fields that specify the date, priorities, and
382 # code bursts that determine Julian Day Number given those groups.
383 # The code in [clock scan] will choose the highest priority
384 # (lowest numbered) set of fields that determines the date.
386 variable DateParseActions {
388 { seconds } 0 {}
390 { julianDay } 1 {}
392 { era century yearOfCentury month dayOfMonth } 2 {
393 dict set date year [expr { 100 * [dict get $date century]
394 + [dict get $date yearOfCentury] }]
395 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
396 $changeover]
398 { era century yearOfCentury dayOfYear } 2 {
399 dict set date year [expr { 100 * [dict get $date century]
400 + [dict get $date yearOfCentury] }]
401 set date [GetJulianDayFromEraYearDay $date[set date {}] \
402 $changeover]
405 { century yearOfCentury month dayOfMonth } 3 {
406 dict set date era CE
407 dict set date year [expr { 100 * [dict get $date century]
408 + [dict get $date yearOfCentury] }]
409 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
410 $changeover]
412 { century yearOfCentury dayOfYear } 3 {
413 dict set date era CE
414 dict set date year [expr { 100 * [dict get $date century]
415 + [dict get $date yearOfCentury] }]
416 set date [GetJulianDayFromEraYearDay $date[set date {}] \
417 $changeover]
419 { iso8601Century iso8601YearOfCentury iso8601Week dayOfWeek } 3 {
420 dict set date era CE
421 dict set date iso8601Year \
422 [expr { 100 * [dict get $date iso8601Century]
423 + [dict get $date iso8601YearOfCentury] }]
424 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
425 $changeover]
428 { yearOfCentury month dayOfMonth } 4 {
429 set date [InterpretTwoDigitYear $date[set date {}] $baseTime]
430 dict set date era CE
431 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
432 $changeover]
434 { yearOfCentury dayOfYear } 4 {
435 set date [InterpretTwoDigitYear $date[set date {}] $baseTime]
436 dict set date era CE
437 set date [GetJulianDayFromEraYearDay $date[set date {}] \
438 $changeover]
440 { iso8601YearOfCentury iso8601Week dayOfWeek } 4 {
441 set date [InterpretTwoDigitYear \
442 $date[set date {}] $baseTime \
443 iso8601YearOfCentury iso8601Year]
444 dict set date era CE
445 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
446 $changeover]
449 { month dayOfMonth } 5 {
450 set date [AssignBaseYear $date[set date {}] \
451 $baseTime $timeZone $changeover]
452 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
453 $changeover]
455 { dayOfYear } 5 {
456 set date [AssignBaseYear $date[set date {}] \
457 $baseTime $timeZone $changeover]
458 set date [GetJulianDayFromEraYearDay $date[set date {}] \
459 $changeover]
461 { iso8601Week dayOfWeek } 5 {
462 set date [AssignBaseIso8601Year $date[set date {}] \
463 $baseTime $timeZone $changeover]
464 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
465 $changeover]
468 { dayOfMonth } 6 {
469 set date [AssignBaseMonth $date[set date {}] \
470 $baseTime $timeZone $changeover]
471 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
472 $changeover]
475 { dayOfWeek } 7 {
476 set date [AssignBaseWeek $date[set date {}] \
477 $baseTime $timeZone $changeover]
478 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
479 $changeover]
482 {} 8 {
483 set date [AssignBaseJulianDay $date[set date {}] \
484 $baseTime $timeZone $changeover]
488 # Groups of fields that specify time of day, priorities,
489 # and code that processes them
491 variable TimeParseActions {
493 seconds 1 {}
495 { hourAMPM minute second amPmIndicator } 2 {
496 dict set date secondOfDay [InterpretHMSP $date]
498 { hour minute second } 2 {
499 dict set date secondOfDay [InterpretHMS $date]
502 { hourAMPM minute amPmIndicator } 3 {
503 dict set date second 0
504 dict set date secondOfDay [InterpretHMSP $date]
506 { hour minute } 3 {
507 dict set date second 0
508 dict set date secondOfDay [InterpretHMS $date]
511 { hourAMPM amPmIndicator } 4 {
512 dict set date minute 0
513 dict set date second 0
514 dict set date secondOfDay [InterpretHMSP $date]
516 { hour } 4 {
517 dict set date minute 0
518 dict set date second 0
519 dict set date secondOfDay [InterpretHMS $date]
522 { } 5 {
523 dict set date secondOfDay 0
527 # Legacy time zones, used primarily for parsing RFC822 dates.
529 variable LegacyTimeZone [dict create \
530 gmt +0000 \
531 ut +0000 \
532 utc +0000 \
533 bst +0100 \
534 wet +0000 \
535 wat -0100 \
536 at -0200 \
537 nft -0330 \
538 nst -0330 \
539 ndt -0230 \
540 ast -0400 \
541 adt -0300 \
542 est -0500 \
543 edt -0400 \
544 cst -0600 \
545 cdt -0500 \
546 mst -0700 \
547 mdt -0600 \
548 pst -0800 \
549 pdt -0700 \
550 yst -0900 \
551 ydt -0800 \
552 hst -1000 \
553 hdt -0900 \
554 cat -1000 \
555 ahst -1000 \
556 nt -1100 \
557 idlw -1200 \
558 cet +0100 \
559 cest +0200 \
560 met +0100 \
561 mewt +0100 \
562 mest +0200 \
563 swt +0100 \
564 sst +0200 \
565 fwt +0100 \
566 fst +0200 \
567 eet +0200 \
568 eest +0300 \
569 bt +0300 \
570 it +0330 \
571 zp4 +0400 \
572 zp5 +0500 \
573 ist +0530 \
574 zp6 +0600 \
575 wast +0700 \
576 wadt +0800 \
577 jt +0730 \
578 cct +0800 \
579 jst +0900 \
580 kst +0900 \
581 cast +0930 \
582 jdt +1000 \
583 kdt +1000 \
584 cadt +1030 \
585 east +1000 \
586 eadt +1030 \
587 gst +1000 \
588 nzt +1200 \
589 nzst +1200 \
590 nzdt +1300 \
591 idle +1200 \
592 a +0100 \
593 b +0200 \
594 c +0300 \
595 d +0400 \
596 e +0500 \
597 f +0600 \
598 g +0700 \
599 h +0800 \
600 i +0900 \
601 k +1000 \
602 l +1100 \
603 m +1200 \
604 n -0100 \
605 o -0200 \
606 p -0300 \
607 q -0400 \
608 r -0500 \
609 s -0600 \
610 t -0700 \
611 u -0800 \
612 v -0900 \
613 w -1000 \
614 x -1100 \
615 y -1200 \
616 z +0000 \
619 # Caches
621 variable LocaleNumeralCache {}; # Dictionary whose keys are locale
622 # names and whose values are pairs
623 # comprising regexes matching numerals
624 # in the given locales and dictionaries
625 # mapping the numerals to their numeric
626 # values.
627 variable McLoaded {}; # Dictionary whose keys are locales
628 # in which [mcload] has been executed
629 # and whose values are second-level
630 # dictionaries indexed by message
631 # name and giving message text.
632 # variable CachedSystemTimeZone; # If 'CachedSystemTimeZone' exists,
633 # it contains the value of the
634 # system time zone, as determined from
635 # the environment.
636 variable TimeZoneBad {}; # Dictionary whose keys are time zone
637 # names and whose values are 1 if
638 # the time zone is unknown and 0
639 # if it is known.
640 variable TZData; # Array whose keys are time zone names
641 # and whose values are lists of quads
642 # comprising start time, UTC offset,
643 # Daylight Saving Time indicator, and
644 # time zone abbreviation.
645 variable FormatProc; # Array mapping format group
646 # and locale to the name of a procedure
647 # that renders the given format
649 ::tcl::clock::Initialize
651 #----------------------------------------------------------------------
653 # clock format --
655 # Formats a count of seconds since the Posix Epoch as a time
656 # of day.
658 # The 'clock format' command formats times of day for output.
659 # Refer to the user documentation to see what it does.
661 #----------------------------------------------------------------------
663 proc ::tcl::clock::format { args } {
665 variable FormatProc
666 variable TZData
668 lassign [ParseFormatArgs {*}$args] format locale timezone
669 set locale [string tolower $locale]
670 set clockval [lindex $args 0]
672 # Get the data for time changes in the given zone
674 if {$timezone eq ""} {
675 set timezone [GetSystemTimeZone]
677 if {![info exists TZData($timezone)]} {
678 if {[catch {SetupTimeZone $timezone} retval opts]} {
679 dict unset opts -errorinfo
680 return -options $opts $retval
684 # Build a procedure to format the result. Cache the built procedure's
685 # name in the 'FormatProc' array to avoid losing its internal
686 # representation, which contains the name resolution.
688 set procName formatproc'$format'$locale
689 set procName [namespace current]::[string map {: {\:} \\ {\\}} $procName]
690 if {[info exists FormatProc($procName)]} {
691 set procName $FormatProc($procName)
692 } else {
693 set FormatProc($procName) \
694 [ParseClockFormatFormat $procName $format $locale]
697 return [$procName $clockval $timezone]
701 #----------------------------------------------------------------------
703 # ParseClockFormatFormat --
705 # Builds and caches a procedure that formats a time value.
707 # Parameters:
708 # format -- Format string to use
709 # locale -- Locale in which the format string is to be interpreted
711 # Results:
712 # Returns the name of the newly-built procedure.
714 #----------------------------------------------------------------------
716 proc ::tcl::clock::ParseClockFormatFormat {procName format locale} {
718 if {[namespace which $procName] ne {}} {
719 return $procName
722 # Map away the locale-dependent composite format groups
724 EnterLocale $locale oldLocale
726 # Change locale if a fresh locale has been given on the command line.
728 set status [catch {
730 ParseClockFormatFormat2 $format $locale $procName
732 } result opts]
734 # Restore the locale
736 if { [info exists oldLocale] } {
737 mclocale $oldLocale
740 # Return either the error or the proc name
742 if { $status == 1 } {
743 if { [lindex [dict get $opts -errorcode] 0] eq {clock} } {
744 return -code error $result
745 } else {
746 return -options $opts $result
748 } else {
749 return $result
754 proc ::tcl::clock::ParseClockFormatFormat2 {format locale procName} {
756 set didLocaleEra 0
757 set didLocaleNumerals 0
758 set preFormatCode \
759 [string map [list @GREGORIAN_CHANGE_DATE@ \
760 [mc GREGORIAN_CHANGE_DATE]] \
762 variable TZData
763 set date [GetDateFields $clockval \
764 $TZData($timezone) \
765 @GREGORIAN_CHANGE_DATE@]
767 set formatString {}
768 set substituents {}
769 set state {}
771 set format [LocalizeFormat $locale $format]
773 foreach char [split $format {}] {
774 switch -exact -- $state {
775 {} {
776 if { [string equal % $char] } {
777 set state percent
778 } else {
779 append formatString $char
782 percent { # Character following a '%' character
783 set state {}
784 switch -exact -- $char {
785 % { # A literal character, '%'
786 append formatString %%
788 a { # Day of week, abbreviated
789 append formatString %s
790 append substituents \
791 [string map \
792 [list @DAYS_OF_WEEK_ABBREV@ \
793 [list [mc DAYS_OF_WEEK_ABBREV]]] \
794 { [lindex @DAYS_OF_WEEK_ABBREV@ \
795 [expr {[dict get $date dayOfWeek] \
796 % 7}]]}]
798 A { # Day of week, spelt out.
799 append formatString %s
800 append substituents \
801 [string map \
802 [list @DAYS_OF_WEEK_FULL@ \
803 [list [mc DAYS_OF_WEEK_FULL]]] \
804 { [lindex @DAYS_OF_WEEK_FULL@ \
805 [expr {[dict get $date dayOfWeek] \
806 % 7}]]}]
808 b - h { # Name of month, abbreviated.
809 append formatString %s
810 append substituents \
811 [string map \
812 [list @MONTHS_ABBREV@ \
813 [list [mc MONTHS_ABBREV]]] \
814 { [lindex @MONTHS_ABBREV@ \
815 [expr {[dict get $date month]-1}]]}]
817 B { # Name of month, spelt out
818 append formatString %s
819 append substituents \
820 [string map \
821 [list @MONTHS_FULL@ \
822 [list [mc MONTHS_FULL]]] \
823 { [lindex @MONTHS_FULL@ \
824 [expr {[dict get $date month]-1}]]}]
826 C { # Century number
827 append formatString %02d
828 append substituents \
829 { [expr {[dict get $date year] / 100}]}
831 d { # Day of month, with leading zero
832 append formatString %02d
833 append substituents { [dict get $date dayOfMonth]}
835 e { # Day of month, without leading zero
836 append formatString %2d
837 append substituents { [dict get $date dayOfMonth]}
839 E { # Format group in a locale-dependent
840 # alternative era
841 set state percentE
842 if {!$didLocaleEra} {
843 append preFormatCode \
844 [string map \
845 [list @LOCALE_ERAS@ \
846 [list [mc LOCALE_ERAS]]] \
848 set date [GetLocaleEra \
849 $date[set date {}] \
850 @LOCALE_ERAS@]}] \n
851 set didLocaleEra 1
853 if {!$didLocaleNumerals} {
854 append preFormatCode \
855 [list set localeNumerals \
856 [mc LOCALE_NUMERALS]] \n
857 set didLocaleNumerals 1
860 g { # Two-digit year relative to ISO8601
861 # week number
862 append formatString %02d
863 append substituents \
864 { [expr { [dict get $date iso8601Year] % 100 }]}
866 G { # Four-digit year relative to ISO8601
867 # week number
868 append formatString %02d
869 append substituents { [dict get $date iso8601Year]}
871 H { # Hour in the 24-hour day, leading zero
872 append formatString %02d
873 append substituents \
874 { [expr { [dict get $date localSeconds] \
875 / 3600 % 24}]}
877 I { # Hour AM/PM, with leading zero
878 append formatString %02d
879 append substituents \
880 { [expr { ( ( ( [dict get $date localSeconds] \
881 % 86400 ) \
882 + 86400 \
883 - 3600 ) \
884 / 3600 ) \
885 % 12 + 1 }] }
887 j { # Day of year (001-366)
888 append formatString %03d
889 append substituents { [dict get $date dayOfYear]}
891 J { # Julian Day Number
892 append formatString %07ld
893 append substituents { [dict get $date julianDay]}
895 k { # Hour (0-23), no leading zero
896 append formatString %2d
897 append substituents \
898 { [expr { [dict get $date localSeconds]
899 / 3600
900 % 24 }]}
902 l { # Hour (12-11), no leading zero
903 append formatString %2d
904 append substituents \
905 { [expr { ( ( ( [dict get $date localSeconds]
906 % 86400 )
907 + 86400
908 - 3600 )
909 / 3600 )
910 % 12 + 1 }]}
912 m { # Month number, leading zero
913 append formatString %02d
914 append substituents { [dict get $date month]}
916 M { # Minute of the hour, leading zero
917 append formatString %02d
918 append substituents \
919 { [expr { [dict get $date localSeconds]
920 / 60
921 % 60 }]}
923 n { # A literal newline
924 append formatString \n
926 N { # Month number, no leading zero
927 append formatString %2d
928 append substituents { [dict get $date month]}
930 O { # A format group in the locale's
931 # alternative numerals
932 set state percentO
933 if {!$didLocaleNumerals} {
934 append preFormatCode \
935 [list set localeNumerals \
936 [mc LOCALE_NUMERALS]] \n
937 set didLocaleNumerals 1
940 p { # Localized 'AM' or 'PM' indicator
941 # converted to uppercase
942 append formatString %s
943 append preFormatCode \
944 [list set AM [string toupper [mc AM]]] \n \
945 [list set PM [string toupper [mc PM]]] \n
946 append substituents \
947 { [expr {(([dict get $date localSeconds]
948 % 86400) < 43200) ?
949 $AM : $PM}]}
951 P { # Localized 'AM' or 'PM' indicator
952 append formatString %s
953 append preFormatCode \
954 [list set am [mc AM]] \n \
955 [list set pm [mc PM]] \n
956 append substituents \
957 { [expr {(([dict get $date localSeconds]
958 % 86400) < 43200) ?
959 $am : $pm}]}
962 Q { # Hi, Jeff!
963 append formatString %s
964 append substituents { [FormatStarDate $date]}
966 s { # Seconds from the Posix Epoch
967 append formatString %s
968 append substituents { [dict get $date seconds]}
970 S { # Second of the minute, with
971 # leading zero
972 append formatString %02d
973 append substituents \
974 { [expr { [dict get $date localSeconds]
975 % 60 }]}
977 t { # A literal tab character
978 append formatString \t
980 u { # Day of the week (1-Monday, 7-Sunday)
981 append formatString %1d
982 append substituents { [dict get $date dayOfWeek]}
984 U { # Week of the year (00-53). The
985 # first Sunday of the year is the
986 # first day of week 01
987 append formatString %02d
988 append preFormatCode {
989 set dow [dict get $date dayOfWeek]
990 if { $dow == 7 } {
991 set dow 0
993 incr dow
994 set UweekNumber \
995 [expr { ( [dict get $date dayOfYear]
996 - $dow + 7 )
997 / 7 }]
999 append substituents { $UweekNumber}
1001 V { # The ISO8601 week number
1002 append formatString %02d
1003 append substituents { [dict get $date iso8601Week]}
1005 w { # Day of the week (0-Sunday,
1006 # 6-Saturday)
1007 append formatString %1d
1008 append substituents \
1009 { [expr { [dict get $date dayOfWeek] % 7 }]}
1011 W { # Week of the year (00-53). The first
1012 # Monday of the year is the first day
1013 # of week 01.
1014 append preFormatCode {
1015 set WweekNumber \
1016 [expr { ( [dict get $date dayOfYear]
1017 - [dict get $date dayOfWeek]
1018 + 7 )
1019 / 7 }]
1021 append formatString %02d
1022 append substituents { $WweekNumber}
1024 y { # The two-digit year of the century
1025 append formatString %02d
1026 append substituents \
1027 { [expr { [dict get $date year] % 100 }]}
1029 Y { # The four-digit year
1030 append formatString %04d
1031 append substituents { [dict get $date year]}
1033 z { # The time zone as hours and minutes
1034 # east (+) or west (-) of Greenwich
1035 append formatString %s
1036 append substituents { [FormatNumericTimeZone \
1037 [dict get $date tzOffset]]}
1039 Z { # The name of the time zone
1040 append formatString %s
1041 append substituents { [dict get $date tzName]}
1043 % { # A literal percent character
1044 append formatString %%
1046 default { # An unknown escape sequence
1047 append formatString %% $char
1051 percentE { # Character following %E
1052 set state {}
1053 switch -exact -- $char {
1055 append formatString %s
1056 append substituents { } \
1057 [string map \
1058 [list @BCE@ [list [mc BCE]] \
1059 @CE@ [list [mc CE]]] \
1060 {[dict get {BCE @BCE@ CE @CE@} \
1061 [dict get $date era]]}]
1063 C { # Locale-dependent era
1064 append formatString %s
1065 append substituents { [dict get $date localeEra]}
1067 y { # Locale-dependent year of the era
1068 append preFormatCode {
1069 set y [dict get $date localeYear]
1070 if { $y >= 0 && $y < 100 } {
1071 set Eyear [lindex $localeNumerals $y]
1072 } else {
1073 set Eyear $y
1076 append formatString %s
1077 append substituents { $Eyear}
1079 default { # Unknown %E format group
1080 append formatString %%E $char
1084 percentO { # Character following %O
1085 set state {}
1086 switch -exact -- $char {
1087 d - e { # Day of the month in alternative
1088 # numerals
1089 append formatString %s
1090 append substituents \
1091 { [lindex $localeNumerals \
1092 [dict get $date dayOfMonth]]}
1094 H - k { # Hour of the day in alternative
1095 # numerals
1096 append formatString %s
1097 append substituents \
1098 { [lindex $localeNumerals \
1099 [expr { [dict get $date localSeconds]
1100 / 3600
1101 % 24 }]]}
1103 I - l { # Hour (12-11) AM/PM in alternative
1104 # numerals
1105 append formatString %s
1106 append substituents \
1107 { [lindex $localeNumerals \
1108 [expr { ( ( ( [dict get $date localSeconds]
1109 % 86400 )
1110 + 86400
1111 - 3600 )
1112 / 3600 )
1113 % 12 + 1 }]]}
1115 m { # Month number in alternative numerals
1116 append formatString %s
1117 append substituents \
1118 { [lindex $localeNumerals [dict get $date month]]}
1120 M { # Minute of the hour in alternative
1121 # numerals
1122 append formatString %s
1123 append substituents \
1124 { [lindex $localeNumerals \
1125 [expr { [dict get $date localSeconds]
1126 / 60
1127 % 60 }]]}
1129 S { # Second of the minute in alternative
1130 # numerals
1131 append formatString %s
1132 append substituents \
1133 { [lindex $localeNumerals \
1134 [expr { [dict get $date localSeconds]
1135 % 60 }]]}
1137 u { # Day of the week (Monday=1,Sunday=7)
1138 # in alternative numerals
1139 append formatString %s
1140 append substituents \
1141 { [lindex $localeNumerals \
1142 [dict get $date dayOfWeek]]}
1144 w { # Day of the week (Sunday=0,Saturday=6)
1145 # in alternative numerals
1146 append formatString %s
1147 append substituents \
1148 { [lindex $localeNumerals \
1149 [expr { [dict get $date dayOfWeek] % 7 }]]}
1151 y { # Year of the century in alternative
1152 # numerals
1153 append formatString %s
1154 append substituents \
1155 { [lindex $localeNumerals \
1156 [expr { [dict get $date year] % 100 }]]}
1158 default { # Unknown format group
1159 append formatString %%O $char
1166 # Clean up any improperly terminated groups
1168 switch -exact -- $state {
1169 percent {
1170 append formatString %%
1172 percentE {
1173 append retval %%E
1175 percentO {
1176 append retval %%O
1180 proc $procName {clockval timezone} "
1181 $preFormatCode
1182 return \[::format [list $formatString] $substituents\]
1185 # puts [list $procName [info args $procName] [info body $procName]]
1187 return $procName
1190 #----------------------------------------------------------------------
1192 # clock scan --
1194 # Inputs a count of seconds since the Posix Epoch as a time
1195 # of day.
1197 # The 'clock format' command scans times of day on input.
1198 # Refer to the user documentation to see what it does.
1200 #----------------------------------------------------------------------
1202 proc ::tcl::clock::scan { args } {
1204 set format {}
1206 # Check the count of args
1208 if { [llength $args] < 1 || [llength $args] % 2 != 1 } {
1209 set cmdName "clock scan"
1210 return -code error \
1211 -errorcode [list CLOCK wrongNumArgs] \
1212 "wrong \# args: should be\
1213 \"$cmdName string\
1214 ?-base seconds?\
1215 ?-format string? ?-gmt boolean?\
1216 ?-locale LOCALE? ?-timezone ZONE?\""
1219 # Set defaults
1221 set base [clock seconds]
1222 set string [lindex $args 0]
1223 set format {}
1224 set gmt 0
1225 set locale c
1226 set timezone [GetSystemTimeZone]
1228 # Pick up command line options.
1230 foreach { flag value } [lreplace $args 0 0] {
1231 set saw($flag) {}
1232 switch -exact -- $flag {
1233 -b - -ba - -bas - -base {
1234 set base $value
1236 -f - -fo - -for - -form - -forma - -format {
1237 set format $value
1239 -g - -gm - -gmt {
1240 set gmt $value
1242 -l - -lo - -loc - -loca - -local - -locale {
1243 set locale [string tolower $value]
1245 -t - -ti - -tim - -time - -timez - -timezo - -timezon - -timezone {
1246 set timezone $value
1248 default {
1249 return -code error \
1250 -errorcode [list CLOCK badSwitch $flag] \
1251 "bad switch \"$flag\",\
1252 must be -base, -format, -gmt, -locale or -timezone"
1257 # Check options for validity
1259 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
1260 return -code error \
1261 -errorcode [list CLOCK gmtWithTimezone] \
1262 "cannot use -gmt and -timezone in same call"
1264 if { [catch { expr { wide($base) } } result] } {
1265 return -code error \
1266 "expected integer but got \"$base\""
1268 if { ![string is boolean $gmt] } {
1269 return -code error \
1270 "expected boolean value but got \"$gmt\""
1271 } else {
1272 if { $gmt } {
1273 set timezone :GMT
1277 if { ![info exists saw(-format)] } {
1278 # Perhaps someday we'll localize the legacy code. Right now,
1279 # it's not localized.
1280 if { [info exists saw(-locale)] } {
1281 return -code error \
1282 -errorcode [list CLOCK flagWithLegacyFormat] \
1283 "legacy \[clock scan\] does not support -locale"
1286 return [FreeScan $string $base $timezone $locale]
1289 # Change locale if a fresh locale has been given on the command line.
1291 EnterLocale $locale oldLocale
1293 set status [catch {
1295 # Map away the locale-dependent composite format groups
1297 set scanner [ParseClockScanFormat $format $locale]
1298 $scanner $string $base $timezone
1300 } result opts]
1302 # Restore the locale
1304 if { [info exists oldLocale] } {
1305 mclocale $oldLocale
1308 if { $status == 1 } {
1309 if { [lindex [dict get $opts -errorcode] 0] eq {clock} } {
1310 return -code error $result
1311 } else {
1312 return -options $opts $result
1314 } else {
1315 return $result
1320 #----------------------------------------------------------------------
1322 # FreeScan --
1324 # Scans a time in free format
1326 # Parameters:
1327 # string - String containing the time to scan
1328 # base - Base time, expressed in seconds from the Epoch
1329 # timezone - Default time zone in which the time will be expressed
1330 # locale - (Unused) Name of the locale where the time will be scanned.
1332 # Results:
1333 # Returns the date and time extracted from the string in seconds
1334 # from the epoch
1336 #----------------------------------------------------------------------
1338 proc ::tcl::clock::FreeScan { string base timezone locale } {
1340 variable TZData
1342 # Get the data for time changes in the given zone
1344 if {[catch {SetupTimeZone $timezone} retval opts]} {
1345 dict unset opts -errorinfo
1346 return -options $opts $retval
1349 # Extract year, month and day from the base time for the
1350 # parser to use as defaults
1352 set date [GetDateFields \
1353 $base \
1354 $TZData($timezone) \
1355 2361222]
1356 dict set date secondOfDay [expr { [dict get $date localSeconds]
1357 % 86400 }]
1359 # Parse the date. The parser will return a list comprising
1360 # date, time, time zone, relative month/day/seconds, relative
1361 # weekday, ordinal month.
1363 set status [catch {
1364 Oldscan $string \
1365 [dict get $date year] \
1366 [dict get $date month] \
1367 [dict get $date dayOfMonth]
1368 } result]
1369 if { $status != 0 } {
1370 return -code error "unable to convert date-time string \"$string\": $result"
1373 lassign $result parseDate parseTime parseZone parseRel \
1374 parseWeekday parseOrdinalMonth
1376 # If the caller supplied a date in the string, update the 'date' dict
1377 # with the value. If the caller didn't specify a time with the date,
1378 # default to midnight.
1380 if { [llength $parseDate] > 0 } {
1381 lassign $parseDate y m d
1382 if { $y < 100 } {
1383 if { $y >= 39 } {
1384 incr y 1900
1385 } else {
1386 incr y 2000
1389 dict set date era CE
1390 dict set date year $y
1391 dict set date month $m
1392 dict set date dayOfMonth $d
1393 if { $parseTime eq {} } {
1394 set parseTime 0
1398 # If the caller supplied a time zone in the string, it comes back
1399 # as a two-element list; the first element is the number of minutes
1400 # east of Greenwich, and the second is a Daylight Saving Time
1401 # indicator ( 1 == yes, 0 == no, -1 == unknown ). We make it into
1402 # a time zone indicator of +-hhmm.
1404 if { [llength $parseZone] > 0 } {
1405 lassign $parseZone minEast dstFlag
1406 set timezone [FormatNumericTimeZone \
1407 [expr { 60 * $minEast + 3600 * $dstFlag }]]
1408 SetupTimeZone $timezone
1410 dict set date tzName $timezone
1412 # Assemble date, time, zone into seconds-from-epoch
1414 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] 2361222]
1415 if { $parseTime ne {} } {
1416 dict set date secondOfDay $parseTime
1417 } elseif { [llength $parseWeekday] != 0
1418 || [llength $parseOrdinalMonth] != 0
1419 || ( [llength $parseRel] != 0
1420 && ( [lindex $parseRel 0] != 0
1421 || [lindex $parseRel 1] != 0 ) ) } {
1422 dict set date secondOfDay 0
1425 dict set date localSeconds \
1426 [expr { -210866803200
1427 + ( 86400 * wide([dict get $date julianDay]) )
1428 + [dict get $date secondOfDay] }]
1429 dict set date tzName $timezone
1430 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) 2361222]
1431 set seconds [dict get $date seconds]
1433 # Do relative times
1435 if { [llength $parseRel] > 0 } {
1436 lassign $parseRel relMonth relDay relSecond
1437 set seconds [add $seconds \
1438 $relMonth months $relDay days $relSecond seconds \
1439 -timezone $timezone -locale $locale]
1442 # Do relative weekday
1444 if { [llength $parseWeekday] > 0 } {
1446 lassign $parseWeekday dayOrdinal dayOfWeek
1447 set date2 [GetDateFields $seconds $TZData($timezone) 2361222]
1448 dict set date2 era CE
1449 set jdwkday [WeekdayOnOrBefore $dayOfWeek \
1450 [expr { [dict get $date2 julianDay]
1451 + 6 }]]
1452 incr jdwkday [expr { 7 * $dayOrdinal }]
1453 if { $dayOrdinal > 0 } {
1454 incr jdwkday -7
1456 dict set date2 secondOfDay \
1457 [expr { [dict get $date2 localSeconds] % 86400 }]
1458 dict set date2 julianDay $jdwkday
1459 dict set date2 localSeconds \
1460 [expr { -210866803200
1461 + ( 86400 * wide([dict get $date2 julianDay]) )
1462 + [dict get $date secondOfDay] }]
1463 dict set date2 tzName $timezone
1464 set date2 [ConvertLocalToUTC $date2[set date2 {}] $TZData($timezone) \
1465 2361222]
1466 set seconds [dict get $date2 seconds]
1470 # Do relative month
1472 if { [llength $parseOrdinalMonth] > 0 } {
1474 lassign $parseOrdinalMonth monthOrdinal monthNumber
1475 if { $monthOrdinal > 0 } {
1476 set monthDiff [expr { $monthNumber - [dict get $date month] }]
1477 if { $monthDiff <= 0 } {
1478 incr monthDiff 12
1480 incr monthOrdinal -1
1481 } else {
1482 set monthDiff [expr { [dict get $date month] - $monthNumber }]
1483 if { $monthDiff >= 0 } {
1484 incr monthDiff -12
1486 incr monthOrdinal
1488 set seconds [add $seconds $monthOrdinal years $monthDiff months \
1489 -timezone $timezone -locale $locale]
1493 return $seconds
1497 #----------------------------------------------------------------------
1499 # ParseClockScanFormat --
1501 # Parses a format string given to [clock scan -format]
1503 # Parameters:
1504 # formatString - The format being parsed
1505 # locale - The current locale
1507 # Results:
1508 # Constructs and returns a procedure that accepts the
1509 # string being scanned, the base time, and the time zone.
1510 # The procedure will either return the scanned time or
1511 # else throw an error that should be rethrown to the caller
1512 # of [clock scan]
1514 # Side effects:
1515 # The given procedure is defined in the ::tcl::clock
1516 # namespace. Scan procedures are not deleted once installed.
1518 # Why do we parse dates by defining a procedure to parse them?
1519 # The reason is that by doing so, we have one convenient place to
1520 # cache all the information: the regular expressions that match the
1521 # patterns (which will be compiled), the code that assembles the
1522 # date information, everything lands in one place. In this way,
1523 # when a given format is reused at run time, all the information
1524 # of how to apply it is available in a single place.
1526 #----------------------------------------------------------------------
1528 proc ::tcl::clock::ParseClockScanFormat {formatString locale} {
1530 # Check whether the format has been parsed previously, and return
1531 # the existing recognizer if it has.
1533 set procName scanproc'$formatString'$locale
1534 set procName [namespace current]::[string map {: {\:} \\ {\\}} $procName]
1535 if { [namespace which $procName] != {} } {
1536 return $procName
1539 variable DateParseActions
1540 variable TimeParseActions
1542 # Localize the %x, %X, etc. groups
1544 set formatString [LocalizeFormat $locale $formatString]
1546 # Condense whitespace
1548 regsub -all {[[:space:]]+} $formatString { } formatString
1550 # Walk through the groups of the format string. In this loop, we
1551 # accumulate:
1552 # - a regular expression that matches the string,
1553 # - the count of capturing brackets in the regexp
1554 # - a set of code that post-processes the fields captured by the regexp,
1555 # - a dictionary whose keys are the names of fields that are present
1556 # in the format string.
1558 set re {^[[:space:]]*}
1559 set captureCount 0
1560 set postcode {}
1561 set fieldSet [dict create]
1562 set fieldCount 0
1563 set postSep {}
1564 set state {}
1566 foreach c [split $formatString {}] {
1567 switch -exact -- $state {
1568 {} {
1569 if { $c eq "%" } {
1570 set state %
1571 } elseif { $c eq " " } {
1572 append re {[[:space:]]+}
1573 } else {
1574 if { ! [string is alnum $c] } {
1575 append re \\
1577 append re $c
1581 set state {}
1582 switch -exact -- $c {
1584 append re %
1586 { } {
1587 append re "\[\[:space:\]\]*"
1589 a - A { # Day of week, in words
1590 set l {}
1591 foreach \
1592 i {7 1 2 3 4 5 6} \
1593 abr [mc DAYS_OF_WEEK_ABBREV] \
1594 full [mc DAYS_OF_WEEK_FULL] {
1595 dict set l [string tolower $abr] $i
1596 dict set l [string tolower $full] $i
1597 incr i
1599 lassign [UniquePrefixRegexp $l] regex lookup
1600 append re ( $regex )
1601 dict set fieldSet dayOfWeek [incr fieldCount]
1602 append postcode "dict set date dayOfWeek \[" \
1603 "dict get " [list $lookup] " " \
1604 \[ {string tolower $field} [incr captureCount] \] \
1605 "\]\n"
1607 b - B - h { # Name of month
1608 set i 0
1609 set l {}
1610 foreach \
1611 abr [mc MONTHS_ABBREV] \
1612 full [mc MONTHS_FULL] {
1613 incr i
1614 dict set l [string tolower $abr] $i
1615 dict set l [string tolower $full] $i
1617 lassign [UniquePrefixRegexp $l] regex lookup
1618 append re ( $regex )
1619 dict set fieldSet month [incr fieldCount]
1620 append postcode "dict set date month \[" \
1621 "dict get " [list $lookup] \
1622 " " \[ {string tolower $field} \
1623 [incr captureCount] \] \
1624 "\]\n"
1626 C { # Gregorian century
1627 append re \\s*(\\d\\d?)
1628 dict set fieldSet century [incr fieldCount]
1629 append postcode "dict set date century \[" \
1630 "::scan \$field" [incr captureCount] " %d" \
1631 "\]\n"
1633 d - e { # Day of month
1634 append re \\s*(\\d\\d?)
1635 dict set fieldSet dayOfMonth [incr fieldCount]
1636 append postcode "dict set date dayOfMonth \[" \
1637 "::scan \$field" [incr captureCount] " %d" \
1638 "\]\n"
1640 E { # Prefix for locale-specific codes
1641 set state %E
1643 g { # ISO8601 2-digit year
1644 append re \\s*(\\d\\d)
1645 dict set fieldSet iso8601YearOfCentury \
1646 [incr fieldCount]
1647 append postcode \
1648 "dict set date iso8601YearOfCentury \[" \
1649 "::scan \$field" [incr captureCount] " %d" \
1650 "\]\n"
1652 G { # ISO8601 4-digit year
1653 append re \\s*(\\d\\d)(\\d\\d)
1654 dict set fieldSet iso8601Century [incr fieldCount]
1655 dict set fieldSet iso8601YearOfCentury \
1656 [incr fieldCount]
1657 append postcode \
1658 "dict set date iso8601Century \[" \
1659 "::scan \$field" [incr captureCount] " %d" \
1660 "\]\n" \
1661 "dict set date iso8601YearOfCentury \[" \
1662 "::scan \$field" [incr captureCount] " %d" \
1663 "\]\n"
1665 H - k { # Hour of day
1666 append re \\s*(\\d\\d?)
1667 dict set fieldSet hour [incr fieldCount]
1668 append postcode "dict set date hour \[" \
1669 "::scan \$field" [incr captureCount] " %d" \
1670 "\]\n"
1672 I - l { # Hour, AM/PM
1673 append re \\s*(\\d\\d?)
1674 dict set fieldSet hourAMPM [incr fieldCount]
1675 append postcode "dict set date hourAMPM \[" \
1676 "::scan \$field" [incr captureCount] " %d" \
1677 "\]\n"
1679 j { # Day of year
1680 append re \\s*(\\d\\d?\\d?)
1681 dict set fieldSet dayOfYear [incr fieldCount]
1682 append postcode "dict set date dayOfYear \[" \
1683 "::scan \$field" [incr captureCount] " %d" \
1684 "\]\n"
1686 J { # Julian Day Number
1687 append re \\s*(\\d+)
1688 dict set fieldSet julianDay [incr fieldCount]
1689 append postcode "dict set date julianDay \[" \
1690 "::scan \$field" [incr captureCount] " %ld" \
1691 "\]\n"
1693 m - N { # Month number
1694 append re \\s*(\\d\\d?)
1695 dict set fieldSet month [incr fieldCount]
1696 append postcode "dict set date month \[" \
1697 "::scan \$field" [incr captureCount] " %d" \
1698 "\]\n"
1700 M { # Minute
1701 append re \\s*(\\d\\d?)
1702 dict set fieldSet minute [incr fieldCount]
1703 append postcode "dict set date minute \[" \
1704 "::scan \$field" [incr captureCount] " %d" \
1705 "\]\n"
1707 n { # Literal newline
1708 append re \\n
1710 O { # Prefix for locale numerics
1711 set state %O
1713 p - P { # AM/PM indicator
1714 set l [list [string tolower [mc AM]] 0 \
1715 [string tolower [mc PM]] 1]
1716 lassign [UniquePrefixRegexp $l] regex lookup
1717 append re ( $regex )
1718 dict set fieldSet amPmIndicator [incr fieldCount]
1719 append postcode "dict set date amPmIndicator \[" \
1720 "dict get " [list $lookup] " \[string tolower " \
1721 "\$field" \
1722 [incr captureCount] \
1723 "\]\]\n"
1725 Q { # Hi, Jeff!
1726 append re {Stardate\s+([-+]?\d+)(\d\d\d)[.](\d)}
1727 incr captureCount
1728 dict set fieldSet seconds [incr fieldCount]
1729 append postcode {dict set date seconds } \[ \
1730 {ParseStarDate $field} [incr captureCount] \
1731 { $field} [incr captureCount] \
1732 { $field} [incr captureCount] \
1733 \] \n
1735 s { # Seconds from Posix Epoch
1736 # This next case is insanely difficult,
1737 # because it's problematic to determine
1738 # whether the field is actually within
1739 # the range of a wide integer.
1740 append re {\s*([-+]?\d+)}
1741 dict set fieldSet seconds [incr fieldCount]
1742 append postcode {dict set date seconds } \[ \
1743 {ScanWide $field} [incr captureCount] \] \n
1745 S { # Second
1746 append re \\s*(\\d\\d?)
1747 dict set fieldSet second [incr fieldCount]
1748 append postcode "dict set date second \[" \
1749 "::scan \$field" [incr captureCount] " %d" \
1750 "\]\n"
1752 t { # Literal tab character
1753 append re \\t
1755 u - w { # Day number within week, 0 or 7 == Sun
1756 # 1=Mon, 6=Sat
1757 append re \\s*(\\d)
1758 dict set fieldSet dayOfWeek [incr fieldCount]
1759 append postcode {::scan $field} [incr captureCount] \
1760 { %d dow} \n \
1762 if { $dow == 0 } {
1763 set dow 7
1764 } elseif { $dow > 7 } {
1765 return -code error \
1766 -errorcode [list CLOCK badDayOfWeek] \
1767 "day of week is greater than 7"
1769 dict set date dayOfWeek $dow
1772 U { # Week of year. The
1773 # first Sunday of the year is the
1774 # first day of week 01. No scan rule
1775 # uses this group.
1776 append re \\s*\\d\\d?
1778 V { # Week of ISO8601 year
1780 append re \\s*(\\d\\d?)
1781 dict set fieldSet iso8601Week [incr fieldCount]
1782 append postcode "dict set date iso8601Week \[" \
1783 "::scan \$field" [incr captureCount] " %d" \
1784 "\]\n"
1786 W { # Week of the year (00-53). The first
1787 # Monday of the year is the first day
1788 # of week 01. No scan rule uses this
1789 # group.
1790 append re \\s*\\d\\d?
1792 y { # Two-digit Gregorian year
1793 append re \\s*(\\d\\d?)
1794 dict set fieldSet yearOfCentury [incr fieldCount]
1795 append postcode "dict set date yearOfCentury \[" \
1796 "::scan \$field" [incr captureCount] " %d" \
1797 "\]\n"
1799 Y { # 4-digit Gregorian year
1800 append re \\s*(\\d\\d)(\\d\\d)
1801 dict set fieldSet century [incr fieldCount]
1802 dict set fieldSet yearOfCentury [incr fieldCount]
1803 append postcode \
1804 "dict set date century \[" \
1805 "::scan \$field" [incr captureCount] " %d" \
1806 "\]\n" \
1807 "dict set date yearOfCentury \[" \
1808 "::scan \$field" [incr captureCount] " %d" \
1809 "\]\n"
1811 z - Z { # Time zone name
1812 append re {(?:([-+]\d\d(?::?\d\d(?::?\d\d)?)?)|([[:alnum:]]{1,4}))}
1813 dict set fieldSet tzName [incr fieldCount]
1814 append postcode \
1815 {if } \{ { $field} [incr captureCount] \
1816 { ne "" } \} { } \{ \n \
1817 {dict set date tzName $field} \
1818 $captureCount \n \
1819 \} { else } \{ \n \
1820 {dict set date tzName } \[ \
1821 {ConvertLegacyTimeZone $field} \
1822 [incr captureCount] \] \n \
1823 \} \n \
1825 % { # Literal percent character
1826 append re %
1828 default {
1829 append re %
1830 if { ! [string is alnum $c] } {
1831 append re \\
1833 append re $c
1837 %E {
1838 switch -exact -- $c {
1839 C { # Locale-dependent era
1840 set d {}
1841 foreach triple [mc LOCALE_ERAS] {
1842 lassign $triple t symbol year
1843 dict set d [string tolower $symbol] $year
1845 lassign [UniquePrefixRegexp $d] regex lookup
1846 append re (?: $regex )
1849 set l {}
1850 dict set l [string tolower [mc BCE]] BCE
1851 dict set l [string tolower [mc CE]] CE
1852 dict set l b.c.e. BCE
1853 dict set l c.e. CE
1854 dict set l b.c. BCE
1855 dict set l a.d. CE
1856 lassign [UniquePrefixRegexp $l] regex lookup
1857 append re ( $regex )
1858 dict set fieldSet era [incr fieldCount]
1859 append postcode "dict set date era \["\
1860 "dict get " [list $lookup] \
1861 { } \[ {string tolower $field} \
1862 [incr captureCount] \] \
1863 "\]\n"
1865 y { # Locale-dependent year of the era
1866 lassign [LocaleNumeralMatcher $locale] regex lookup
1867 append re $regex
1868 incr captureCount
1870 default {
1871 append re %E
1872 if { ! [string is alnum $c] } {
1873 append re \\
1875 append re $c
1878 set state {}
1880 %O {
1881 switch -exact -- $c {
1882 d - e {
1883 lassign [LocaleNumeralMatcher $locale] regex lookup
1884 append re $regex
1885 dict set fieldSet dayOfMonth [incr fieldCount]
1886 append postcode "dict set date dayOfMonth \[" \
1887 "dict get " [list $lookup] " \$field" \
1888 [incr captureCount] \
1889 "\]\n"
1891 H - k {
1892 lassign [LocaleNumeralMatcher $locale] regex lookup
1893 append re $regex
1894 dict set fieldSet hour [incr fieldCount]
1895 append postcode "dict set date hour \[" \
1896 "dict get " [list $lookup] " \$field" \
1897 [incr captureCount] \
1898 "\]\n"
1900 I - l {
1901 lassign [LocaleNumeralMatcher $locale] regex lookup
1902 append re $regex
1903 dict set fieldSet hourAMPM [incr fieldCount]
1904 append postcode "dict set date hourAMPM \[" \
1905 "dict get " [list $lookup] " \$field" \
1906 [incr captureCount] \
1907 "\]\n"
1910 lassign [LocaleNumeralMatcher $locale] regex lookup
1911 append re $regex
1912 dict set fieldSet month [incr fieldCount]
1913 append postcode "dict set date month \[" \
1914 "dict get " [list $lookup] " \$field" \
1915 [incr captureCount] \
1916 "\]\n"
1919 lassign [LocaleNumeralMatcher $locale] regex lookup
1920 append re $regex
1921 dict set fieldSet minute [incr fieldCount]
1922 append postcode "dict set date minute \[" \
1923 "dict get " [list $lookup] " \$field" \
1924 [incr captureCount] \
1925 "\]\n"
1928 lassign [LocaleNumeralMatcher $locale] regex lookup
1929 append re $regex
1930 dict set fieldSet second [incr fieldCount]
1931 append postcode "dict set date second \[" \
1932 "dict get " [list $lookup] " \$field" \
1933 [incr captureCount] \
1934 "\]\n"
1936 u - w {
1937 lassign [LocaleNumeralMatcher $locale] regex lookup
1938 append re $regex
1939 dict set fieldSet dayOfWeek [incr fieldCount]
1940 append postcode "set dow \[dict get " [list $lookup] \
1941 { $field} [incr captureCount] \] \n \
1943 if { $dow == 0 } {
1944 set dow 7
1945 } elseif { $dow > 7 } {
1946 return -code error \
1947 -errorcode [list CLOCK badDayOfWeek] \
1948 "day of week is greater than 7"
1950 dict set date dayOfWeek $dow
1954 lassign [LocaleNumeralMatcher $locale] regex lookup
1955 append re $regex
1956 dict set fieldSet yearOfCentury [incr fieldCount]
1957 append postcode {dict set date yearOfCentury } \[ \
1958 {dict get } [list $lookup] { $field} \
1959 [incr captureCount] \] \n
1961 default {
1962 append re %O
1963 if { ! [string is alnum $c] } {
1964 append re \\
1966 append re $c
1969 set state {}
1974 # Clean up any unfinished format groups
1976 append re $state \\s*\$
1978 # Build the procedure
1980 set procBody {}
1981 append procBody "variable ::tcl::clock::TZData" \n
1982 append procBody "if \{ !\[ regexp -nocase [list $re] \$string ->"
1983 for { set i 1 } { $i <= $captureCount } { incr i } {
1984 append procBody " " field $i
1986 append procBody "\] \} \{" \n
1987 append procBody {
1988 return -code error -errorcode [list CLOCK badInputString] \
1989 {input string does not match supplied format}
1991 append procBody \}\n
1992 append procBody "set date \[dict create\]" \n
1993 append procBody {dict set date tzName $timeZone} \n
1994 append procBody $postcode
1995 append procBody [list set changeover [mc GREGORIAN_CHANGE_DATE]] \n
1997 # Get time zone if needed
1999 if { ![dict exists $fieldSet seconds]
2000 && ![dict exists $fieldSet starDate] } {
2001 if { [dict exists $fieldSet tzName] } {
2002 append procBody {
2003 set timeZone [dict get $date tzName]
2006 append procBody {
2007 ::tcl::clock::SetupTimeZone $timeZone
2011 # Add code that gets Julian Day Number from the fields.
2013 append procBody [MakeParseCodeFromFields $fieldSet $DateParseActions]
2015 # Get time of day
2017 append procBody [MakeParseCodeFromFields $fieldSet $TimeParseActions]
2019 # Assemble seconds, and convert local nominal time to UTC.
2021 if { ![dict exists $fieldSet seconds]
2022 && ![dict exists $fieldSet starDate] } {
2023 append procBody {
2024 if { [dict get $date julianDay] > 5373484 } {
2025 return -code error -errorcode [list CLOCK dateTooLarge] \
2026 "requested date too large to represent"
2028 dict set date localSeconds \
2029 [expr { -210866803200
2030 + ( 86400 * wide([dict get $date julianDay]) )
2031 + [dict get $date secondOfDay] }]
2033 append procBody {
2034 set date [::tcl::clock::ConvertLocalToUTC $date[set date {}] \
2035 $TZData($timeZone) \
2036 $changeover]
2040 # Return result
2042 append procBody {return [dict get $date seconds]} \n
2044 proc $procName { string baseTime timeZone } $procBody
2046 # puts [list proc $procName [list string baseTime timeZone] $procBody]
2048 return $procName
2051 #----------------------------------------------------------------------
2053 # LocaleNumeralMatcher --
2055 # Composes a regexp that captures the numerals in the given
2056 # locale, and a dictionary to map them to conventional numerals.
2058 # Parameters:
2059 # locale - Name of the current locale
2061 # Results:
2062 # Returns a two-element list comprising the regexp and the
2063 # dictionary.
2065 # Side effects:
2066 # Caches the result.
2068 #----------------------------------------------------------------------
2070 proc ::tcl::clock::LocaleNumeralMatcher {l} {
2072 variable LocaleNumeralCache
2074 if { ![dict exists $LocaleNumeralCache $l] } {
2075 set d {}
2076 set i 0
2077 set sep \(
2078 foreach n [mc LOCALE_NUMERALS] {
2079 dict set d $n $i
2080 regsub -all {[^[:alnum:]]} $n \\\\& subex
2081 append re $sep $subex
2082 set sep |
2083 incr i
2085 append re \)
2086 dict set LocaleNumeralCache $l [list $re $d]
2088 return [dict get $LocaleNumeralCache $l]
2093 #----------------------------------------------------------------------
2095 # UniquePrefixRegexp --
2097 # Composes a regexp that performs unique-prefix matching. The
2098 # RE matches one of a supplied set of strings, or any unique
2099 # prefix thereof.
2101 # Parameters:
2102 # data - List of alternating match-strings and values.
2103 # Match-strings with distinct values are considered
2104 # distinct.
2106 # Results:
2107 # Returns a two-element list. The first is a regexp that
2108 # matches any unique prefix of any of the strings. The second
2109 # is a dictionary whose keys are match values from the regexp
2110 # and whose values are the corresponding values from 'data'.
2112 # Side effects:
2113 # None.
2115 #----------------------------------------------------------------------
2117 proc ::tcl::clock::UniquePrefixRegexp { data } {
2119 # The 'successors' dictionary will contain, for each string that
2120 # is a prefix of any key, all characters that may follow that
2121 # prefix. The 'prefixMapping' dictionary will have keys that
2122 # are prefixes of keys and values that correspond to the keys.
2124 set prefixMapping [dict create]
2125 set successors [dict create {} {}]
2127 # Walk the key-value pairs
2129 foreach { key value } $data {
2131 # Construct all prefixes of the key;
2133 set prefix {}
2134 foreach char [split $key {}] {
2135 set oldPrefix $prefix
2136 dict set successors $oldPrefix $char {}
2137 append prefix $char
2139 # Put the prefixes in the 'prefixMapping' and 'successors'
2140 # dictionaries
2142 dict lappend prefixMapping $prefix $value
2143 if { ![dict exists $successors $prefix] } {
2144 dict set successors $prefix {}
2149 # Identify those prefixes that designate unique values, and
2150 # those that are the full keys
2152 set uniquePrefixMapping {}
2153 dict for { key valueList } $prefixMapping {
2154 if { [llength $valueList] == 1 } {
2155 dict set uniquePrefixMapping $key [lindex $valueList 0]
2158 foreach { key value } $data {
2159 dict set uniquePrefixMapping $key $value
2162 # Construct the re.
2164 return [list \
2165 [MakeUniquePrefixRegexp $successors $uniquePrefixMapping {}] \
2166 $uniquePrefixMapping]
2169 #----------------------------------------------------------------------
2171 # MakeUniquePrefixRegexp --
2173 # Service procedure for 'UniquePrefixRegexp' that constructs
2174 # a regular expresison that matches the unique prefixes.
2176 # Parameters:
2177 # successors - Dictionary whose keys are all prefixes
2178 # of keys passed to 'UniquePrefixRegexp' and whose
2179 # values are dictionaries whose keys are the characters
2180 # that may follow those prefixes.
2181 # uniquePrefixMapping - Dictionary whose keys are the unique
2182 # prefixes and whose values are not examined.
2183 # prefixString - Current prefix being processed.
2185 # Results:
2186 # Returns a constructed regular expression that matches the set
2187 # of unique prefixes beginning with the 'prefixString'.
2189 # Side effects:
2190 # None.
2192 #----------------------------------------------------------------------
2194 proc ::tcl::clock::MakeUniquePrefixRegexp { successors
2195 uniquePrefixMapping
2196 prefixString } {
2198 # Get the characters that may follow the current prefix string
2200 set schars [lsort -ascii [dict keys [dict get $successors $prefixString]]]
2201 if { [llength $schars] == 0 } {
2202 return {}
2205 # If there is more than one successor character, or if the current
2206 # prefix is a unique prefix, surround the generated re with non-capturing
2207 # parentheses.
2209 set re {}
2210 if { [dict exists $uniquePrefixMapping $prefixString]
2211 || [llength $schars] > 1 } {
2212 append re "(?:"
2215 # Generate a regexp that matches the successors.
2217 set sep ""
2218 foreach { c } $schars {
2219 set nextPrefix $prefixString$c
2220 regsub -all {[^[:alnum:]]} $c \\\\& rechar
2221 append re $sep $rechar \
2222 [MakeUniquePrefixRegexp \
2223 $successors $uniquePrefixMapping $nextPrefix]
2224 set sep |
2227 # If the current prefix is a unique prefix, make all following text
2228 # optional. Otherwise, if there is more than one successor character,
2229 # close the non-capturing parentheses.
2231 if { [dict exists $uniquePrefixMapping $prefixString] } {
2232 append re ")?"
2233 } elseif { [llength $schars] > 1 } {
2234 append re ")"
2237 return $re
2240 #----------------------------------------------------------------------
2242 # MakeParseCodeFromFields --
2244 # Composes Tcl code to extract the Julian Day Number from a
2245 # dictionary containing date fields.
2247 # Parameters:
2248 # dateFields -- Dictionary whose keys are fields of the date,
2249 # and whose values are the rightmost positions
2250 # at which those fields appear.
2251 # parseActions -- List of triples: field set, priority, and
2252 # code to emit. Smaller priorities are better, and
2253 # the list must be in ascending order by priority
2255 # Results:
2256 # Returns a burst of code that extracts the day number from the
2257 # given date.
2259 # Side effects:
2260 # None.
2262 #----------------------------------------------------------------------
2264 proc ::tcl::clock::MakeParseCodeFromFields { dateFields parseActions } {
2266 set currPrio 999
2267 set currFieldPos [list]
2268 set currCodeBurst {
2269 error "in ::tcl::clock::MakeParseCodeFromFields: can't happen"
2272 foreach { fieldSet prio parseAction } $parseActions {
2274 # If we've found an answer that's better than any that follow,
2275 # quit now.
2277 if { $prio > $currPrio } {
2278 break
2281 # Accumulate the field positions that are used in the current
2282 # field grouping.
2284 set fieldPos [list]
2285 set ok true
2286 foreach field $fieldSet {
2287 if { ! [dict exists $dateFields $field] } {
2288 set ok 0
2289 break
2291 lappend fieldPos [dict get $dateFields $field]
2294 # Quit if we don't have a complete set of fields
2295 if { !$ok } {
2296 continue
2299 # Determine whether the current answer is better than the last.
2301 set fPos [lsort -integer -decreasing $fieldPos]
2303 if { $prio == $currPrio } {
2304 foreach currPos $currFieldPos newPos $fPos {
2305 if { ![string is integer $newPos]
2306 || ![string is integer $currPos]
2307 || $newPos > $currPos } {
2308 break
2310 if { $newPos < $currPos } {
2311 set ok 0
2312 break
2316 if { !$ok } {
2317 continue
2320 # Remember the best possibility for extracting date information
2322 set currPrio $prio
2323 set currFieldPos $fPos
2324 set currCodeBurst $parseAction
2328 return $currCodeBurst
2332 #----------------------------------------------------------------------
2334 # EnterLocale --
2336 # Switch [mclocale] to a given locale if necessary
2338 # Parameters:
2339 # locale -- Desired locale
2340 # oldLocaleVar -- Name of a variable in caller's scope that
2341 # tracks the previous locale name.
2343 # Results:
2344 # Returns the locale that was previously current.
2346 # Side effects:
2347 # Does [mclocale]. If necessary, uses [mcload] to load the
2348 # designated locale's files, and tracks that it has done so
2349 # in the 'McLoaded' variable.
2351 #----------------------------------------------------------------------
2353 proc ::tcl::clock::EnterLocale { locale oldLocaleVar } {
2355 upvar 1 $oldLocaleVar oldLocale
2357 variable MsgDir
2358 variable McLoaded
2360 set oldLocale [mclocale]
2361 if { $locale eq {system} } {
2363 if { $::tcl_platform(platform) ne {windows} } {
2365 # On a non-windows platform, the 'system' locale is
2366 # the same as the 'current' locale
2368 set locale current
2369 } else {
2371 # On a windows platform, the 'system' locale is
2372 # adapted from the 'current' locale by applying the
2373 # date and time formats from the Control Panel.
2374 # First, load the 'current' locale if it's not yet loaded
2376 if {![dict exists $McLoaded $oldLocale] } {
2377 mcload $MsgDir
2378 dict set McLoaded $oldLocale {}
2381 # Make a new locale string for the system locale, and
2382 # get the Control Panel information
2384 set locale ${oldLocale}_windows
2385 if { ![dict exists $McLoaded $locale] } {
2386 LoadWindowsDateTimeFormats $locale
2387 dict set McLoaded $locale {}
2391 if { $locale eq {current}} {
2392 set locale $oldLocale
2393 unset oldLocale
2394 } elseif { $locale eq $oldLocale } {
2395 unset oldLocale
2396 } else {
2397 mclocale $locale
2399 if { ![dict exists $McLoaded $locale] } {
2400 mcload $MsgDir
2401 dict set McLoaded $locale {}
2406 #----------------------------------------------------------------------
2408 # LoadWindowsDateTimeFormats --
2410 # Load the date/time formats from the Control Panel in Windows
2411 # and convert them so that they're usable by Tcl.
2413 # Parameters:
2414 # locale - Name of the locale in whose message catalog
2415 # the converted formats are to be stored.
2417 # Results:
2418 # None.
2420 # Side effects:
2421 # Updates the given message catalog with the locale strings.
2423 # Presumes that on entry, [mclocale] is set to the current locale,
2424 # so that default strings can be obtained if the Registry query
2425 # fails.
2427 #----------------------------------------------------------------------
2429 proc ::tcl::clock::LoadWindowsDateTimeFormats { locale } {
2431 # Bail out if we can't find the Registry
2433 variable NoRegistry
2434 if { [info exists NoRegistry] } return
2436 if { ![catch {
2437 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2438 sShortDate
2439 } string] } {
2440 set quote {}
2441 set datefmt {}
2442 foreach { unquoted quoted } [split $string '] {
2443 append datefmt $quote [string map {
2444 dddd %A
2445 ddd %a
2446 dd %d
2447 d %e
2448 MMMM %B
2449 MMM %b
2450 MM %m
2451 M %N
2452 yyyy %Y
2453 yy %y
2454 y %y
2455 gg {}
2456 } $unquoted]
2457 if { $quoted eq {} } {
2458 set quote '
2459 } else {
2460 set quote $quoted
2463 ::msgcat::mcset $locale DATE_FORMAT $datefmt
2466 if { ![catch {
2467 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2468 sLongDate
2469 } string] } {
2470 set quote {}
2471 set ldatefmt {}
2472 foreach { unquoted quoted } [split $string '] {
2473 append ldatefmt $quote [string map {
2474 dddd %A
2475 ddd %a
2476 dd %d
2477 d %e
2478 MMMM %B
2479 MMM %b
2480 MM %m
2481 M %N
2482 yyyy %Y
2483 yy %y
2484 y %y
2485 gg {}
2486 } $unquoted]
2487 if { $quoted eq {} } {
2488 set quote '
2489 } else {
2490 set quote $quoted
2493 ::msgcat::mcset $locale LOCALE_DATE_FORMAT $ldatefmt
2496 if { ![catch {
2497 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2498 sTimeFormat
2499 } string] } {
2500 set quote {}
2501 set timefmt {}
2502 foreach { unquoted quoted } [split $string '] {
2503 append timefmt $quote [string map {
2504 HH %H
2505 H %k
2506 hh %I
2507 h %l
2508 mm %M
2509 m %M
2510 ss %S
2511 s %S
2512 tt %p
2513 t %p
2514 } $unquoted]
2515 if { $quoted eq {} } {
2516 set quote '
2517 } else {
2518 set quote $quoted
2521 ::msgcat::mcset $locale TIME_FORMAT $timefmt
2524 catch {
2525 ::msgcat::mcset $locale DATE_TIME_FORMAT "$datefmt $timefmt"
2527 catch {
2528 ::msgcat::mcset $locale LOCALE_DATE_TIME_FORMAT "$ldatefmt $timefmt"
2531 return
2535 #----------------------------------------------------------------------
2537 # LocalizeFormat --
2539 # Map away locale-dependent format groups in a clock format.
2541 # Parameters:
2542 # locale -- Current [mclocale] locale, supplied to avoid
2543 # an extra call
2544 # format -- Format supplied to [clock scan] or [clock format]
2546 # Results:
2547 # Returns the string with locale-dependent composite format
2548 # groups substituted out.
2550 # Side effects:
2551 # None.
2553 #----------------------------------------------------------------------
2555 proc ::tcl::clock::LocalizeFormat { locale format } {
2557 variable McLoaded
2559 if { [dict exists $McLoaded $locale FORMAT $format] } {
2560 return [dict get $McLoaded $locale FORMAT $format]
2562 set inFormat $format
2564 # Handle locale-dependent format groups by mapping them out of the format
2565 # string. Note that the order of the [string map] operations is
2566 # significant because later formats can refer to later ones; for example
2567 # %c can refer to %X, which in turn can refer to %T.
2569 set list {
2570 %% %%
2571 %D %m/%d/%Y
2572 %+ {%a %b %e %H:%M:%S %Z %Y}
2574 lappend list %EY [string map $list [mc LOCALE_YEAR_FORMAT]]
2575 lappend list %T [string map $list [mc TIME_FORMAT_24_SECS]]
2576 lappend list %R [string map $list [mc TIME_FORMAT_24]]
2577 lappend list %r [string map $list [mc TIME_FORMAT_12]]
2578 lappend list %X [string map $list [mc TIME_FORMAT]]
2579 lappend list %EX [string map $list [mc LOCALE_TIME_FORMAT]]
2580 lappend list %x [string map $list [mc DATE_FORMAT]]
2581 lappend list %Ex [string map $list [mc LOCALE_DATE_FORMAT]]
2582 lappend list %c [string map $list [mc DATE_TIME_FORMAT]]
2583 lappend list %Ec [string map $list [mc LOCALE_DATE_TIME_FORMAT]]
2584 set format [string map $list $format]
2586 dict set McLoaded $locale FORMAT $inFormat $format
2587 return $format
2590 #----------------------------------------------------------------------
2592 # FormatNumericTimeZone --
2594 # Formats a time zone as +hhmmss
2596 # Parameters:
2597 # z - Time zone in seconds east of Greenwich
2599 # Results:
2600 # Returns the time zone formatted in a numeric form
2602 # Side effects:
2603 # None.
2605 #----------------------------------------------------------------------
2607 proc ::tcl::clock::FormatNumericTimeZone { z } {
2609 if { $z < 0 } {
2610 set z [expr { - $z }]
2611 set retval -
2612 } else {
2613 set retval +
2615 append retval [::format %02d [expr { $z / 3600 }]]
2616 set z [expr { $z % 3600 }]
2617 append retval [::format %02d [expr { $z / 60 }]]
2618 set z [expr { $z % 60 }]
2619 if { $z != 0 } {
2620 append retval [::format %02d $z]
2622 return $retval
2626 #----------------------------------------------------------------------
2628 # FormatStarDate --
2630 # Formats a date as a StarDate.
2632 # Parameters:
2633 # date - Dictionary containing 'year', 'dayOfYear', and
2634 # 'localSeconds' fields.
2636 # Results:
2637 # Returns the given date formatted as a StarDate.
2639 # Side effects:
2640 # None.
2642 # Jeff Hobbs put this in to support an atrocious pun about Tcl being
2643 # "Enterprise ready." Now we're stuck with it.
2645 #----------------------------------------------------------------------
2647 proc ::tcl::clock::FormatStarDate { date } {
2649 variable Roddenberry
2651 # Get day of year, zero based
2653 set doy [expr { [dict get $date dayOfYear] - 1 }]
2655 # Determine whether the year is a leap year
2657 set lp [IsGregorianLeapYear $date]
2659 # Convert day of year to a fractional year
2661 if { $lp } {
2662 set fractYear [expr { 1000 * $doy / 366 }]
2663 } else {
2664 set fractYear [expr { 1000 * $doy / 365 }]
2667 # Put together the StarDate
2669 return [::format "Stardate %02d%03d.%1d" \
2670 [expr { [dict get $date year] - $Roddenberry }] \
2671 $fractYear \
2672 [expr { [dict get $date localSeconds] % 86400
2673 / ( 86400 / 10 ) }]]
2676 #----------------------------------------------------------------------
2678 # ParseStarDate --
2680 # Parses a StarDate
2682 # Parameters:
2683 # year - Year from the Roddenberry epoch
2684 # fractYear - Fraction of a year specifiying the day of year.
2685 # fractDay - Fraction of a day
2687 # Results:
2688 # Returns a count of seconds from the Posix epoch.
2690 # Side effects:
2691 # None.
2693 # Jeff Hobbs put this in to support an atrocious pun about Tcl being
2694 # "Enterprise ready." Now we're stuck with it.
2696 #----------------------------------------------------------------------
2698 proc ::tcl::clock::ParseStarDate { year fractYear fractDay } {
2700 variable Roddenberry
2702 # Build a tentative date from year and fraction.
2704 set date [dict create \
2705 gregorian 1 \
2706 era CE \
2707 year [expr { $year + $Roddenberry }] \
2708 dayOfYear [expr { $fractYear * 365 / 1000 + 1 }]]
2709 set date [GetJulianDayFromGregorianEraYearDay $date[set date {}]]
2711 # Determine whether the given year is a leap year
2713 set lp [IsGregorianLeapYear $date]
2715 # Reconvert the fractional year according to whether the given
2716 # year is a leap year
2718 if { $lp } {
2719 dict set date dayOfYear \
2720 [expr { $fractYear * 366 / 1000 + 1 }]
2721 } else {
2722 dict set date dayOfYear \
2723 [expr { $fractYear * 365 / 1000 + 1 }]
2725 dict unset date julianDay
2726 dict unset date gregorian
2727 set date [GetJulianDayFromGregorianEraYearDay $date[set date {}]]
2729 return [expr { 86400 * [dict get $date julianDay]
2730 - 210866803200
2731 + ( 86400 / 10 ) * $fractDay }]
2735 #----------------------------------------------------------------------
2737 # ScanWide --
2739 # Scans a wide integer from an input
2741 # Parameters:
2742 # str - String containing a decimal wide integer
2744 # Results:
2745 # Returns the string as a pure wide integer. Throws an error if
2746 # the string is misformatted or out of range.
2748 #----------------------------------------------------------------------
2750 proc ::tcl::clock::ScanWide { str } {
2751 set count [::scan $str {%ld %c} result junk]
2752 if { $count != 1 } {
2753 return -code error -errorcode [list CLOCK notAnInteger $str] \
2754 "\"$str\" is not an integer"
2756 if { [incr result 0] != $str } {
2757 return -code error -errorcode [list CLOCK integervalueTooLarge] \
2758 "integer value too large to represent"
2760 return $result
2763 #----------------------------------------------------------------------
2765 # InterpretTwoDigitYear --
2767 # Given a date that contains only the year of the century,
2768 # determines the target value of a two-digit year.
2770 # Parameters:
2771 # date - Dictionary containing fields of the date.
2772 # baseTime - Base time relative to which the date is expressed.
2773 # twoDigitField - Name of the field that stores the two-digit year.
2774 # Default is 'yearOfCentury'
2775 # fourDigitField - Name of the field that will receive the four-digit
2776 # year. Default is 'year'
2778 # Results:
2779 # Returns the dictionary augmented with the four-digit year, stored in
2780 # the given key.
2782 # Side effects:
2783 # None.
2785 # The current rule for interpreting a two-digit year is that the year
2786 # shall be between 1937 and 2037, thus staying within the range of a
2787 # 32-bit signed value for time. This rule may change to a sliding
2788 # window in future versions, so the 'baseTime' parameter (which is
2789 # currently ignored) is provided in the procedure signature.
2791 #----------------------------------------------------------------------
2793 proc ::tcl::clock::InterpretTwoDigitYear { date baseTime
2794 { twoDigitField yearOfCentury }
2795 { fourDigitField year } } {
2797 set yr [dict get $date $twoDigitField]
2798 if { $yr <= 37 } {
2799 dict set date $fourDigitField [expr { $yr + 2000 }]
2800 } else {
2801 dict set date $fourDigitField [expr { $yr + 1900 }]
2803 return $date
2807 #----------------------------------------------------------------------
2809 # AssignBaseYear --
2811 # Places the number of the current year into a dictionary.
2813 # Parameters:
2814 # date - Dictionary value to update
2815 # baseTime - Base time from which to extract the year, expressed
2816 # in seconds from the Posix epoch
2817 # timezone - the time zone in which the date is being scanned
2818 # changeover - the Julian Day on which the Gregorian calendar
2819 # was adopted in the target locale.
2821 # Results:
2822 # Returns the dictionary with the current year assigned.
2824 # Side effects:
2825 # None.
2827 #----------------------------------------------------------------------
2829 proc ::tcl::clock::AssignBaseYear { date baseTime timezone changeover } {
2831 variable TZData
2833 # Find the Julian Day Number corresponding to the base time, and
2834 # find the Gregorian year corresponding to that Julian Day.
2836 set date2 [GetDateFields $baseTime $TZData($timezone) $changeover]
2838 # Store the converted year
2840 dict set date era [dict get $date2 era]
2841 dict set date year [dict get $date2 year]
2843 return $date
2847 #----------------------------------------------------------------------
2849 # AssignBaseIso8601Year --
2851 # Determines the base year in the ISO8601 fiscal calendar.
2853 # Parameters:
2854 # date - Dictionary containing the fields of the date that
2855 # is to be augmented with the base year.
2856 # baseTime - Base time expressed in seconds from the Posix epoch.
2857 # timeZone - Target time zone
2858 # changeover - Julian Day of adoption of the Gregorian calendar in
2859 # the target locale.
2861 # Results:
2862 # Returns the given date with "iso8601Year" set to the
2863 # base year.
2865 # Side effects:
2866 # None.
2868 #----------------------------------------------------------------------
2870 proc ::tcl::clock::AssignBaseIso8601Year {date baseTime timeZone changeover} {
2872 variable TZData
2874 # Find the Julian Day Number corresponding to the base time
2876 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2878 # Calculate the ISO8601 date and transfer the year
2880 dict set date era CE
2881 dict set date iso8601Year [dict get $date2 iso8601Year]
2882 return $date
2885 #----------------------------------------------------------------------
2887 # AssignBaseMonth --
2889 # Places the number of the current year and month into a
2890 # dictionary.
2892 # Parameters:
2893 # date - Dictionary value to update
2894 # baseTime - Time from which the year and month are to be
2895 # obtained, expressed in seconds from the Posix epoch.
2896 # timezone - Name of the desired time zone
2897 # changeover - Julian Day on which the Gregorian calendar was adopted.
2899 # Results:
2900 # Returns the dictionary with the base year and month assigned.
2902 # Side effects:
2903 # None.
2905 #----------------------------------------------------------------------
2907 proc ::tcl::clock::AssignBaseMonth {date baseTime timezone changeover} {
2909 variable TZData
2911 # Find the year and month corresponding to the base time
2913 set date2 [GetDateFields $baseTime $TZData($timezone) $changeover]
2914 dict set date era [dict get $date2 era]
2915 dict set date year [dict get $date2 year]
2916 dict set date month [dict get $date2 month]
2917 return $date
2921 #----------------------------------------------------------------------
2923 # AssignBaseWeek --
2925 # Determines the base year and week in the ISO8601 fiscal calendar.
2927 # Parameters:
2928 # date - Dictionary containing the fields of the date that
2929 # is to be augmented with the base year and week.
2930 # baseTime - Base time expressed in seconds from the Posix epoch.
2931 # changeover - Julian Day on which the Gregorian calendar was adopted
2932 # in the target locale.
2934 # Results:
2935 # Returns the given date with "iso8601Year" set to the
2936 # base year and "iso8601Week" to the week number.
2938 # Side effects:
2939 # None.
2941 #----------------------------------------------------------------------
2943 proc ::tcl::clock::AssignBaseWeek {date baseTime timeZone changeover} {
2945 variable TZData
2947 # Find the Julian Day Number corresponding to the base time
2949 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2951 # Calculate the ISO8601 date and transfer the year
2953 dict set date era CE
2954 dict set date iso8601Year [dict get $date2 iso8601Year]
2955 dict set date iso8601Week [dict get $date2 iso8601Week]
2956 return $date
2959 #----------------------------------------------------------------------
2961 # AssignBaseJulianDay --
2963 # Determines the base day for a time-of-day conversion.
2965 # Parameters:
2966 # date - Dictionary that is to get the base day
2967 # baseTime - Base time expressed in seconds from the Posix epoch
2968 # changeover - Julian day on which the Gregorian calendar was
2969 # adpoted in the target locale.
2971 # Results:
2972 # Returns the given dictionary augmented with a 'julianDay' field
2973 # that contains the base day.
2975 # Side effects:
2976 # None.
2978 #----------------------------------------------------------------------
2980 proc ::tcl::clock::AssignBaseJulianDay { date baseTime timeZone changeover } {
2982 variable TZData
2984 # Find the Julian Day Number corresponding to the base time
2986 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2987 dict set date julianDay [dict get $date2 julianDay]
2989 return $date
2992 #----------------------------------------------------------------------
2994 # InterpretHMSP --
2996 # Interprets a time in the form "hh:mm:ss am".
2998 # Parameters:
2999 # date -- Dictionary containing "hourAMPM", "minute", "second"
3000 # and "amPmIndicator" fields.
3002 # Results:
3003 # Returns the number of seconds from local midnight.
3005 # Side effects:
3006 # None.
3008 #----------------------------------------------------------------------
3010 proc ::tcl::clock::InterpretHMSP { date } {
3012 set hr [dict get $date hourAMPM]
3013 if { $hr == 12 } {
3014 set hr 0
3016 if { [dict get $date amPmIndicator] } {
3017 incr hr 12
3019 dict set date hour $hr
3020 return [InterpretHMS $date[set date {}]]
3024 #----------------------------------------------------------------------
3026 # InterpretHMS --
3028 # Interprets a 24-hour time "hh:mm:ss"
3030 # Parameters:
3031 # date -- Dictionary containing the "hour", "minute" and "second"
3032 # fields.
3034 # Results:
3035 # Returns the given dictionary augmented with a "secondOfDay"
3036 # field containing the number of seconds from local midnight.
3038 # Side effects:
3039 # None.
3041 #----------------------------------------------------------------------
3043 proc ::tcl::clock::InterpretHMS { date } {
3045 return [expr { ( [dict get $date hour] * 60
3046 + [dict get $date minute] ) * 60
3047 + [dict get $date second] }]
3051 #----------------------------------------------------------------------
3053 # GetSystemTimeZone --
3055 # Determines the system time zone, which is the default for the
3056 # 'clock' command if no other zone is supplied.
3058 # Parameters:
3059 # None.
3061 # Results:
3062 # Returns the system time zone.
3064 # Side effects:
3065 # Stores the sustem time zone in the 'CachedSystemTimeZone'
3066 # variable, since determining it may be an expensive process.
3068 #----------------------------------------------------------------------
3070 proc ::tcl::clock::GetSystemTimeZone {} {
3072 variable CachedSystemTimeZone
3073 variable TimeZoneBad
3075 if {[set result [getenv TCL_TZ]] ne {}} {
3076 set timezone $result
3077 } elseif {[set result [getenv TZ]] ne {}} {
3078 set timezone $result
3080 if {![info exists timezone]} {
3081 # Cache the time zone only if it was detected by one of the
3082 # expensive methods.
3083 if { [info exists CachedSystemTimeZone] } {
3084 set timezone $CachedSystemTimeZone
3085 } elseif { $::tcl_platform(platform) eq {windows} } {
3086 set timezone [GuessWindowsTimeZone]
3087 } elseif { [file exists /etc/localtime]
3088 && ![catch {ReadZoneinfoFile \
3089 Tcl/Localtime /etc/localtime}] } {
3090 set timezone :Tcl/Localtime
3091 } else {
3092 set timezone :localtime
3094 set CachedSystemTimeZone $timezone
3096 if { ![dict exists $TimeZoneBad $timezone] } {
3097 dict set TimeZoneBad $timezone [catch {SetupTimeZone $timezone}]
3099 if { [dict get $TimeZoneBad $timezone] } {
3100 return :localtime
3101 } else {
3102 return $timezone
3107 #----------------------------------------------------------------------
3109 # ConvertLegacyTimeZone --
3111 # Given an alphanumeric time zone identifier and the system
3112 # time zone, convert the alphanumeric identifier to an
3113 # unambiguous time zone.
3115 # Parameters:
3116 # tzname - Name of the time zone to convert
3118 # Results:
3119 # Returns a time zone name corresponding to tzname, but
3120 # in an unambiguous form, generally +hhmm.
3122 # This procedure is implemented primarily to allow the parsing of
3123 # RFC822 date/time strings. Processing a time zone name on input
3124 # is not recommended practice, because there is considerable room
3125 # for ambiguity; for instance, is BST Brazilian Standard Time, or
3126 # British Summer Time?
3128 #----------------------------------------------------------------------
3130 proc ::tcl::clock::ConvertLegacyTimeZone { tzname } {
3132 variable LegacyTimeZone
3134 set tzname [string tolower $tzname]
3135 if { ![dict exists $LegacyTimeZone $tzname] } {
3136 return -code error -errorcode [list CLOCK badTZName $tzname] \
3137 "time zone \"$tzname\" not found"
3138 } else {
3139 return [dict get $LegacyTimeZone $tzname]
3144 #----------------------------------------------------------------------
3146 # SetupTimeZone --
3148 # Given the name or specification of a time zone, sets up
3149 # its in-memory data.
3151 # Parameters:
3152 # tzname - Name of a time zone
3154 # Results:
3155 # Unless the time zone is ':localtime', sets the TZData array
3156 # to contain the lookup table for local<->UTC conversion.
3157 # Returns an error if the time zone cannot be parsed.
3159 #----------------------------------------------------------------------
3161 proc ::tcl::clock::SetupTimeZone { timezone } {
3163 variable TZData
3165 if {! [info exists TZData($timezone)] } {
3166 variable MINWIDE
3167 if { $timezone eq {:localtime} } {
3169 # Nothing to do, we'll convert using the localtime function
3171 } elseif { [regexp {^([-+])(\d\d)(?::?(\d\d)(?::?(\d\d))?)?} $timezone \
3172 -> s hh mm ss] } {
3174 # Make a fixed offset
3176 ::scan $hh %d hh
3177 if { $mm eq {} } {
3178 set mm 0
3179 } else {
3180 ::scan $mm %d mm
3182 if { $ss eq {} } {
3183 set ss 0
3184 } else {
3185 ::scan $ss %d ss
3187 set offset [expr { ( $hh * 60 + $mm ) * 60 + $ss }]
3188 if { $s eq {-} } {
3189 set offset [expr { - $offset }]
3191 set TZData($timezone) [list [list $MINWIDE $offset -1 $timezone]]
3193 } elseif { [string index $timezone 0] eq {:} } {
3195 # Convert using a time zone file
3197 if {
3198 [catch {
3199 LoadTimeZoneFile [string range $timezone 1 end]
3201 && [catch {
3202 LoadZoneinfoFile [string range $timezone 1 end]
3205 return -code error \
3206 -errorcode [list CLOCK badTimeZone $timezone] \
3207 "time zone \"$timezone\" not found"
3210 } elseif { ![catch {ParsePosixTimeZone $timezone} tzfields] } {
3212 # This looks like a POSIX time zone - try to process it
3214 if { [catch {ProcessPosixTimeZone $tzfields} data opts] } {
3215 if { [lindex [dict get $opts -errorcode] 0] eq {CLOCK} } {
3216 dict unset opts -errorinfo
3218 return -options $opts $data
3219 } else {
3220 set TZData($timezone) $data
3223 } else {
3225 # We couldn't parse this as a POSIX time zone. Try
3226 # again with a time zone file - this time without a colon
3228 if { [catch { LoadTimeZoneFile $timezone }]
3229 && [catch { LoadZoneinfoFile $timezone } - opts] } {
3230 dict unset opts -errorinfo
3231 return -options $opts "time zone $timezone not found"
3233 set TZData($timezone) $TZData(:$timezone)
3237 return
3240 #----------------------------------------------------------------------
3242 # GuessWindowsTimeZone --
3244 # Determines the system time zone on windows.
3246 # Parameters:
3247 # None.
3249 # Results:
3250 # Returns a time zone specifier that corresponds to the system
3251 # time zone information found in the Registry.
3253 # Bugs:
3254 # Fixed dates for DST change are unimplemented at present, because
3255 # no time zone information supplied with Windows actually uses
3256 # them!
3258 # On a Windows system where neither $env(TCL_TZ) nor $env(TZ) is
3259 # specified, GuessWindowsTimeZone looks in the Registry for the
3260 # system time zone information. It then attempts to find an entry
3261 # in WinZoneInfo for a time zone that uses the same rules. If
3262 # it finds one, it returns it; otherwise, it constructs a Posix-style
3263 # time zone string and returns that.
3265 #----------------------------------------------------------------------
3267 proc ::tcl::clock::GuessWindowsTimeZone {} {
3269 variable WinZoneInfo
3270 variable NoRegistry
3271 variable TimeZoneBad
3273 if { [info exists NoRegistry] } {
3274 return :localtime
3277 # Dredge time zone information out of the registry
3279 if { [catch {
3280 set rpath HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\TimeZoneInformation
3281 set data [list \
3282 [expr { -60
3283 * [registry get $rpath Bias] }] \
3284 [expr { -60
3285 * [registry get $rpath StandardBias] }] \
3286 [expr { -60 \
3287 * [registry get $rpath DaylightBias] }]]
3288 set stdtzi [registry get $rpath StandardStart]
3289 foreach ind {0 2 14 4 6 8 10 12} {
3290 binary scan $stdtzi @${ind}s val
3291 lappend data $val
3293 set daytzi [registry get $rpath DaylightStart]
3294 foreach ind {0 2 14 4 6 8 10 12} {
3295 binary scan $daytzi @${ind}s val
3296 lappend data $val
3298 }] } {
3300 # Missing values in the Registry - bail out
3302 return :localtime
3305 # Make up a Posix time zone specifier if we can't find one.
3306 # Check here that the tzdata file exists, in case we're running
3307 # in an environment (e.g. starpack) where tzdata is incomplete.
3308 # (Bug 1237907)
3310 if { [dict exists $WinZoneInfo $data] } {
3311 set tzname [dict get $WinZoneInfo $data]
3312 if { ! [dict exists $TimeZoneBad $tzname] } {
3313 dict set TimeZoneBad $tzname [catch {SetupTimeZone $tzname}]
3315 } else {
3316 set tzname {}
3318 if { $tzname eq {} || [dict get $TimeZoneBad $tzname] } {
3319 lassign $data \
3320 bias stdBias dstBias \
3321 stdYear stdMonth stdDayOfWeek stdDayOfMonth \
3322 stdHour stdMinute stdSecond stdMillisec \
3323 dstYear dstMonth dstDayOfWeek dstDayOfMonth \
3324 dstHour dstMinute dstSecond dstMillisec
3325 set stdDelta [expr { $bias + $stdBias }]
3326 set dstDelta [expr { $bias + $dstBias }]
3327 if { $stdDelta <= 0 } {
3328 set stdSignum +
3329 set stdDelta [expr { - $stdDelta }]
3330 set dispStdSignum -
3331 } else {
3332 set stdSignum -
3333 set dispStdSignum +
3335 set hh [::format %02d [expr { $stdDelta / 3600 }]]
3336 set mm [::format %02d [expr { ($stdDelta / 60 ) % 60 }]]
3337 set ss [::format %02d [expr { $stdDelta % 60 }]]
3338 set tzname {}
3339 append tzname < $dispStdSignum $hh $mm > $stdSignum $hh : $mm : $ss
3340 if { $stdMonth >= 0 } {
3341 if { $dstDelta <= 0 } {
3342 set dstSignum +
3343 set dstDelta [expr { - $dstDelta }]
3344 set dispDstSignum -
3345 } else {
3346 set dstSignum -
3347 set dispDstSignum +
3349 set hh [::format %02d [expr { $dstDelta / 3600 }]]
3350 set mm [::format %02d [expr { ($dstDelta / 60 ) % 60 }]]
3351 set ss [::format %02d [expr { $dstDelta % 60 }]]
3352 append tzname < $dispDstSignum $hh $mm > $dstSignum $hh : $mm : $ss
3353 if { $dstYear == 0 } {
3354 append tzname ,M $dstMonth . $dstDayOfMonth . $dstDayOfWeek
3355 } else {
3356 # I have not been able to find any locale on which
3357 # Windows converts time zone on a fixed day of the year,
3358 # hence don't know how to interpret the fields.
3359 # If someone can inform me, I'd be glad to code it up.
3360 # For right now, we bail out in such a case.
3361 return :localtime
3363 append tzname / [::format %02d $dstHour] \
3364 : [::format %02d $dstMinute] \
3365 : [::format %02d $dstSecond]
3366 if { $stdYear == 0 } {
3367 append tzname ,M $stdMonth . $stdDayOfMonth . $stdDayOfWeek
3368 } else {
3369 # I have not been able to find any locale on which
3370 # Windows converts time zone on a fixed day of the year,
3371 # hence don't know how to interpret the fields.
3372 # If someone can inform me, I'd be glad to code it up.
3373 # For right now, we bail out in such a case.
3374 return :localtime
3376 append tzname / [::format %02d $stdHour] \
3377 : [::format %02d $stdMinute] \
3378 : [::format %02d $stdSecond]
3380 dict set WinZoneInfo $data $tzname
3383 return [dict get $WinZoneInfo $data]
3387 #----------------------------------------------------------------------
3389 # LoadTimeZoneFile --
3391 # Load the data file that specifies the conversion between a
3392 # given time zone and Greenwich.
3394 # Parameters:
3395 # fileName -- Name of the file to load
3397 # Results:
3398 # None.
3400 # Side effects:
3401 # TZData(:fileName) contains the time zone data
3403 #----------------------------------------------------------------------
3405 proc ::tcl::clock::LoadTimeZoneFile { fileName } {
3406 variable DataDir
3407 variable TZData
3409 if { [info exists TZData($fileName)] } {
3410 return
3413 # Since an unsafe interp uses the [clock] command in the master,
3414 # this code is security sensitive. Make sure that the path name
3415 # cannot escape the given directory.
3417 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3418 return -code error \
3419 -errorcode [list CLOCK badTimeZone $:fileName] \
3420 "time zone \":$fileName\" not valid"
3422 if { [catch {
3423 source -encoding utf-8 [file join $DataDir $fileName]
3424 }] } {
3425 return -code error \
3426 -errorcode [list CLOCK badTimeZone :$fileName] \
3427 "time zone \":$fileName\" not found"
3429 return
3432 #----------------------------------------------------------------------
3434 # LoadZoneinfoFile --
3436 # Loads a binary time zone information file in Olson format.
3438 # Parameters:
3439 # fileName - Relative path name of the file to load.
3441 # Results:
3442 # Returns an empty result normally; returns an error if no
3443 # Olson file was found or the file was malformed in some way.
3445 # Side effects:
3446 # TZData(:fileName) contains the time zone data
3448 #----------------------------------------------------------------------
3450 proc ::tcl::clock::LoadZoneinfoFile { fileName } {
3452 variable ZoneinfoPaths
3454 # Since an unsafe interp uses the [clock] command in the master,
3455 # this code is security sensitive. Make sure that the path name
3456 # cannot escape the given directory.
3458 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3459 return -code error \
3460 -errorcode [list CLOCK badTimeZone $:fileName] \
3461 "time zone \":$fileName\" not valid"
3463 foreach d $ZoneinfoPaths {
3464 set fname [file join $d $fileName]
3465 if { [file readable $fname] && [file isfile $fname] } {
3466 break
3468 unset fname
3470 ReadZoneinfoFile $fileName $fname
3473 #----------------------------------------------------------------------
3475 # ReadZoneinfoFile --
3477 # Loads a binary time zone information file in Olson format.
3479 # Parameters:
3480 # fileName - Name of the time zone (relative path name of the
3481 # file).
3482 # fname - Absolute path name of the file.
3484 # Results:
3485 # Returns an empty result normally; returns an error if no
3486 # Olson file was found or the file was malformed in some way.
3488 # Side effects:
3489 # TZData(:fileName) contains the time zone data
3491 #----------------------------------------------------------------------
3494 proc ::tcl::clock::ReadZoneinfoFile {fileName fname} {
3495 variable MINWIDE
3496 variable TZData
3497 if { ![file exists $fname] } {
3498 return -code error "$fileName not found"
3501 if { [file size $fname] > 262144 } {
3502 return -code error "$fileName too big"
3505 # Suck in all the data from the file
3507 set f [open $fname r]
3508 fconfigure $f -translation binary
3509 set d [read $f]
3510 close $f
3512 # The file begins with a magic number, sixteen reserved bytes,
3513 # and then six 4-byte integers giving counts of fileds in the file.
3515 binary scan $d a4a1x15IIIIII \
3516 magic version nIsGMT nIsStd nLeap nTime nType nChar
3517 set seek 44
3518 set ilen 4
3519 set iformat I
3520 if { $magic != {TZif} } {
3521 return -code error "$fileName not a time zone information file"
3523 if { $nType > 255 } {
3524 return -code error "$fileName contains too many time types"
3526 # Accept only Posix-style zoneinfo. Sorry, 'leaps' bigots.
3527 if { $nLeap != 0 } {
3528 return -code error "$fileName contains leap seconds"
3531 # In a version 2 file, we use the second part of the file, which
3532 # contains 64-bit transition times.
3534 if {$version eq "2"} {
3535 set seek [expr {44
3536 + 5 * $nTime
3537 + 6 * $nType
3538 + 4 * $nLeap
3539 + $nIsStd
3540 + $nIsGMT
3541 + $nChar
3543 binary scan $d @${seek}a4a1x15IIIIII \
3544 magic version nIsGMT nIsStd nLeap nTime nType nChar
3545 if {$magic ne {TZif}} {
3546 return -code error "seek address $seek miscomputed, magic = $magic"
3548 set iformat W
3549 set ilen 8
3550 incr seek 44
3553 # Next come ${nTime} transition times, followed by ${nTime} time type
3554 # codes. The type codes are unsigned 1-byte quantities. We insert an
3555 # arbitrary start time in front of the transitions.
3557 binary scan $d @${seek}${iformat}${nTime}c${nTime} times tempCodes
3558 incr seek [expr { ($ilen + 1) * $nTime }]
3559 set times [linsert $times 0 $MINWIDE]
3560 set codes {}
3561 foreach c $tempCodes {
3562 lappend codes [expr { $c & 0xff }]
3564 set codes [linsert $codes 0 0]
3566 # Next come ${nType} time type descriptions, each of which has an
3567 # offset (seconds east of GMT), a DST indicator, and an index into
3568 # the abbreviation text.
3570 for { set i 0 } { $i < $nType } { incr i } {
3571 binary scan $d @${seek}Icc gmtOff isDst abbrInd
3572 lappend types [list $gmtOff $isDst $abbrInd]
3573 incr seek 6
3576 # Next come $nChar characters of time zone name abbreviations,
3577 # which are null-terminated.
3578 # We build them up into a dictionary indexed by character index,
3579 # because that's what's in the indices above.
3581 binary scan $d @${seek}a${nChar} abbrs
3582 incr seek ${nChar}
3583 set abbrList [split $abbrs \0]
3584 set i 0
3585 set abbrevs {}
3586 foreach a $abbrList {
3587 for {set j 0} {$j <= [string length $a]} {incr j} {
3588 dict set abbrevs $i [string range $a $j end]
3589 incr i
3593 # Package up a list of tuples, each of which contains transition time,
3594 # seconds east of Greenwich, DST flag and time zone abbreviation.
3596 set r {}
3597 set lastTime $MINWIDE
3598 foreach t $times c $codes {
3599 if { $t < $lastTime } {
3600 return -code error "$fileName has times out of order"
3602 set lastTime $t
3603 lassign [lindex $types $c] gmtoff isDst abbrInd
3604 set abbrev [dict get $abbrevs $abbrInd]
3605 lappend r [list $t $gmtoff $isDst $abbrev]
3608 # In a version 2 file, there is also a POSIX-style time zone description
3609 # at the very end of the file. To get to it, skip over
3610 # nLeap leap second values (8 bytes each),
3611 # nIsStd standard/DST indicators and nIsGMT UTC/local indicators.
3613 if {$version eq {2}} {
3614 set seek [expr {$seek + 8 * $nLeap + $nIsStd + $nIsGMT + 1}]
3615 set last [string first \n $d $seek]
3616 set posix [string range $d $seek [expr {$last-1}]]
3617 if {[llength $posix] > 0} {
3618 set posixFields [ParsePosixTimeZone $posix]
3619 foreach tuple [ProcessPosixTimeZone $posixFields] {
3620 lassign $tuple t gmtoff isDst abbrev
3621 if {$t > $lastTime} {
3622 lappend r $tuple
3628 set TZData(:$fileName) $r
3630 return
3633 #----------------------------------------------------------------------
3635 # ParsePosixTimeZone --
3637 # Parses the TZ environment variable in Posix form
3639 # Parameters:
3640 # tz Time zone specifier to be interpreted
3642 # Results:
3643 # Returns a dictionary whose values contain the various pieces of
3644 # the time zone specification.
3646 # Side effects:
3647 # None.
3649 # Errors:
3650 # Throws an error if the syntax of the time zone is incorrect.
3652 # The following keys are present in the dictionary:
3653 # stdName - Name of the time zone when Daylight Saving Time
3654 # is not in effect.
3655 # stdSignum - Sign (+, -, or empty) of the offset from Greenwich
3656 # to the given (non-DST) time zone. + and the empty
3657 # string denote zones west of Greenwich, - denotes east
3658 # of Greenwich; this is contrary to the ISO convention
3659 # but follows Posix.
3660 # stdHours - Hours part of the offset from Greenwich to the given
3661 # (non-DST) time zone.
3662 # stdMinutes - Minutes part of the offset from Greenwich to the
3663 # given (non-DST) time zone. Empty denotes zero.
3664 # stdSeconds - Seconds part of the offset from Greenwich to the
3665 # given (non-DST) time zone. Empty denotes zero.
3666 # dstName - Name of the time zone when DST is in effect, or the
3667 # empty string if the time zone does not observe Daylight
3668 # Saving Time.
3669 # dstSignum, dstHours, dstMinutes, dstSeconds -
3670 # Fields corresponding to stdSignum, stdHours, stdMinutes,
3671 # stdSeconds for the Daylight Saving Time version of the
3672 # time zone. If dstHours is empty, it is presumed to be 1.
3673 # startDayOfYear - The ordinal number of the day of the year on which
3674 # Daylight Saving Time begins. If this field is
3675 # empty, then DST begins on a given month-week-day,
3676 # as below.
3677 # startJ - The letter J, or an empty string. If a J is present in
3678 # this field, then startDayOfYear does not count February 29
3679 # even in leap years.
3680 # startMonth - The number of the month in which Daylight Saving Time
3681 # begins, supplied if startDayOfYear is empty. If both
3682 # startDayOfYear and startMonth are empty, then US rules
3683 # are presumed.
3684 # startWeekOfMonth - The number of the week in the month in which
3685 # Daylight Saving Time begins, in the range 1-5.
3686 # 5 denotes the last week of the month even in a
3687 # 4-week month.
3688 # startDayOfWeek - The number of the day of the week (Sunday=0,
3689 # Saturday=6) on which Daylight Saving Time begins.
3690 # startHours - The hours part of the time of day at which Daylight
3691 # Saving Time begins. An empty string is presumed to be 2.
3692 # startMinutes - The minutes part of the time of day at which DST begins.
3693 # An empty string is presumed zero.
3694 # startSeconds - The seconds part of the time of day at which DST begins.
3695 # An empty string is presumed zero.
3696 # endDayOfYear, endJ, endMonth, endWeekOfMonth, endDayOfWeek,
3697 # endHours, endMinutes, endSeconds -
3698 # Specify the end of DST in the same way that the start* fields
3699 # specify the beginning of DST.
3701 # This procedure serves only to break the time specifier into fields.
3702 # No attempt is made to canonicalize the fields or supply default values.
3704 #----------------------------------------------------------------------
3706 proc ::tcl::clock::ParsePosixTimeZone { tz } {
3708 if {[regexp -expanded -nocase -- {
3710 # 1 - Standard time zone name
3711 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3712 # 2 - Standard time zone offset, signum
3713 ([-+]?)
3714 # 3 - Standard time zone offset, hours
3715 ([[:digit:]]{1,2})
3717 # 4 - Standard time zone offset, minutes
3718 : ([[:digit:]]{1,2})
3719 (?:
3720 # 5 - Standard time zone offset, seconds
3721 : ([[:digit:]]{1,2} )
3725 # 6 - DST time zone name
3726 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3729 # 7 - DST time zone offset, signum
3730 ([-+]?)
3731 # 8 - DST time zone offset, hours
3732 ([[:digit:]]{1,2})
3734 # 9 - DST time zone offset, minutes
3735 : ([[:digit:]]{1,2})
3736 (?:
3737 # 10 - DST time zone offset, seconds
3738 : ([[:digit:]]{1,2})
3745 # 11 - Optional J in n and Jn form 12 - Day of year
3746 ( J ? ) ( [[:digit:]]+ )
3748 # 13 - Month number 14 - Week of month 15 - Day of week
3749 ( [[:digit:]] + )
3750 [.] ( [[:digit:]] + )
3751 [.] ( [[:digit:]] + )
3754 # 16 - Start time of DST - hours
3755 / ( [[:digit:]]{1,2} )
3757 # 17 - Start time of DST - minutes
3758 : ( [[:digit:]]{1,2} )
3760 # 18 - Start time of DST - seconds
3761 : ( [[:digit:]]{1,2} )
3767 # 19 - Optional J in n and Jn form 20 - Day of year
3768 ( J ? ) ( [[:digit:]]+ )
3770 # 21 - Month number 22 - Week of month 23 - Day of week
3771 ( [[:digit:]] + )
3772 [.] ( [[:digit:]] + )
3773 [.] ( [[:digit:]] + )
3776 # 24 - End time of DST - hours
3777 / ( [[:digit:]]{1,2} )
3779 # 25 - End time of DST - minutes
3780 : ( [[:digit:]]{1,2} )
3782 # 26 - End time of DST - seconds
3783 : ( [[:digit:]]{1,2} )
3791 } $tz -> x(stdName) x(stdSignum) x(stdHours) x(stdMinutes) x(stdSeconds) \
3792 x(dstName) x(dstSignum) x(dstHours) x(dstMinutes) x(dstSeconds) \
3793 x(startJ) x(startDayOfYear) \
3794 x(startMonth) x(startWeekOfMonth) x(startDayOfWeek) \
3795 x(startHours) x(startMinutes) x(startSeconds) \
3796 x(endJ) x(endDayOfYear) \
3797 x(endMonth) x(endWeekOfMonth) x(endDayOfWeek) \
3798 x(endHours) x(endMinutes) x(endSeconds)] } {
3800 # it's a good timezone
3802 return [array get x]
3804 } else {
3806 return -code error\
3807 -errorcode [list CLOCK badTimeZone $tz] \
3808 "unable to parse time zone specification \"$tz\""
3814 #----------------------------------------------------------------------
3816 # ProcessPosixTimeZone --
3818 # Handle a Posix time zone after it's been broken out into
3819 # fields.
3821 # Parameters:
3822 # z - Dictionary returned from 'ParsePosixTimeZone'
3824 # Results:
3825 # Returns time zone information for the 'TZData' array.
3827 # Side effects:
3828 # None.
3830 #----------------------------------------------------------------------
3832 proc ::tcl::clock::ProcessPosixTimeZone { z } {
3834 variable MINWIDE
3835 variable TZData
3837 # Determine the standard time zone name and seconds east of Greenwich
3839 set stdName [dict get $z stdName]
3840 if { [string index $stdName 0] eq {<} } {
3841 set stdName [string range $stdName 1 end-1]
3843 if { [dict get $z stdSignum] eq {-} } {
3844 set stdSignum +1
3845 } else {
3846 set stdSignum -1
3848 set stdHours [lindex [::scan [dict get $z stdHours] %d] 0]
3849 if { [dict get $z stdMinutes] ne {} } {
3850 set stdMinutes [lindex [::scan [dict get $z stdMinutes] %d] 0]
3851 } else {
3852 set stdMinutes 0
3854 if { [dict get $z stdSeconds] ne {} } {
3855 set stdSeconds [lindex [::scan [dict get $z stdSeconds] %d] 0]
3856 } else {
3857 set stdSeconds 0
3859 set stdOffset [expr { ( ( $stdHours * 60 + $stdMinutes )
3860 * 60 + $stdSeconds )
3861 * $stdSignum }]
3862 set data [list [list $MINWIDE $stdOffset 0 $stdName]]
3864 # If there's no daylight zone, we're done
3866 set dstName [dict get $z dstName]
3867 if { $dstName eq {} } {
3868 return $data
3870 if { [string index $dstName 0] eq {<} } {
3871 set dstName [string range $dstName 1 end-1]
3874 # Determine the daylight name
3876 if { [dict get $z dstSignum] eq {-} } {
3877 set dstSignum +1
3878 } else {
3879 set dstSignum -1
3881 if { [dict get $z dstHours] eq {} } {
3882 set dstOffset [expr { 3600 + $stdOffset }]
3883 } else {
3884 set dstHours [lindex [::scan [dict get $z dstHours] %d] 0]
3885 if { [dict get $z dstMinutes] ne {} } {
3886 set dstMinutes [lindex [::scan [dict get $z dstMinutes] %d] 0]
3887 } else {
3888 set dstMinutes 0
3890 if { [dict get $z dstSeconds] ne {} } {
3891 set dstSeconds [lindex [::scan [dict get $z dstSeconds] %d] 0]
3892 } else {
3893 set dstSeconds 0
3895 set dstOffset [expr { ( ( $dstHours * 60 + $dstMinutes )
3896 * 60 + $dstSeconds )
3897 * $dstSignum }]
3900 # Fill in defaults for European or US DST rules
3901 # US start time is the second Sunday in March
3902 # EU start time is the last Sunday in March
3903 # US end time is the first Sunday in November.
3904 # EU end time is the last Sunday in October
3906 if { [dict get $z startDayOfYear] eq {}
3907 && [dict get $z startMonth] eq {} } {
3908 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3909 # EU
3910 dict set z startWeekOfMonth 5
3911 if {$stdHours>2} {
3912 dict set z startHours 2
3913 } else {
3914 dict set z startHours [expr {$stdHours+1}]
3916 } else {
3917 # US
3918 dict set z startWeekOfMonth 2
3919 dict set z startHours 2
3921 dict set z startMonth 3
3922 dict set z startDayOfWeek 0
3923 dict set z startMinutes 0
3924 dict set z startSeconds 0
3926 if { [dict get $z endDayOfYear] eq {}
3927 && [dict get $z endMonth] eq {} } {
3928 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3929 # EU
3930 dict set z endMonth 10
3931 dict set z endWeekOfMonth 5
3932 if {$stdHours>2} {
3933 dict set z endHours 3
3934 } else {
3935 dict set z endHours [expr {$stdHours+2}]
3937 } else {
3938 # US
3939 dict set z endMonth 11
3940 dict set z endWeekOfMonth 1
3941 dict set z endHours 2
3943 dict set z endDayOfWeek 0
3944 dict set z endMinutes 0
3945 dict set z endSeconds 0
3948 # Put DST in effect in all years from 1916 to 2099.
3950 for { set y 1916 } { $y < 2099 } { incr y } {
3951 set startTime [DeterminePosixDSTTime $z start $y]
3952 incr startTime [expr { - wide($stdOffset) }]
3953 set endTime [DeterminePosixDSTTime $z end $y]
3954 incr endTime [expr { - wide($dstOffset) }]
3955 if { $startTime < $endTime } {
3956 lappend data \
3957 [list $startTime $dstOffset 1 $dstName] \
3958 [list $endTime $stdOffset 0 $stdName]
3959 } else {
3960 lappend data \
3961 [list $endTime $stdOffset 0 $stdName] \
3962 [list $startTime $dstOffset 1 $dstName]
3966 return $data
3970 #----------------------------------------------------------------------
3972 # DeterminePosixDSTTime --
3974 # Determines the time that Daylight Saving Time starts or ends
3975 # from a Posix time zone specification.
3977 # Parameters:
3978 # z - Time zone data returned from ParsePosixTimeZone.
3979 # Missing fields are expected to be filled in with
3980 # default values.
3981 # bound - The word 'start' or 'end'
3982 # y - The year for which the transition time is to be determined.
3984 # Results:
3985 # Returns the transition time as a count of seconds from
3986 # the epoch. The time is relative to the wall clock, not UTC.
3988 #----------------------------------------------------------------------
3990 proc ::tcl::clock::DeterminePosixDSTTime { z bound y } {
3992 variable FEB_28
3994 # Determine the start or end day of DST
3996 set date [dict create era CE year $y]
3997 set doy [dict get $z ${bound}DayOfYear]
3998 if { $doy ne {} } {
4000 # Time was specified as a day of the year
4002 if { [dict get $z ${bound}J] ne {}
4003 && [IsGregorianLeapYear $y]
4004 && ( $doy > $FEB_28 ) } {
4005 incr doy
4007 dict set date dayOfYear $doy
4008 set date [GetJulianDayFromEraYearDay $date[set date {}] 2361222]
4009 } else {
4011 # Time was specified as a day of the week within a month
4013 dict set date month [dict get $z ${bound}Month]
4014 dict set date dayOfWeek [dict get $z ${bound}DayOfWeek]
4015 set dowim [dict get $z ${bound}WeekOfMonth]
4016 if { $dowim >= 5 } {
4017 set dowim -1
4019 dict set date dayOfWeekInMonth $dowim
4020 set date [GetJulianDayFromEraYearMonthWeekDay $date[set date {}] 2361222]
4024 set jd [dict get $date julianDay]
4025 set seconds [expr { wide($jd) * wide(86400)
4026 - wide(210866803200) }]
4028 set h [dict get $z ${bound}Hours]
4029 if { $h eq {} } {
4030 set h 2
4031 } else {
4032 set h [lindex [::scan $h %d] 0]
4034 set m [dict get $z ${bound}Minutes]
4035 if { $m eq {} } {
4036 set m 0
4037 } else {
4038 set m [lindex [::scan $m %d] 0]
4040 set s [dict get $z ${bound}Seconds]
4041 if { $s eq {} } {
4042 set s 0
4043 } else {
4044 set s [lindex [::scan $s %d] 0]
4046 set tod [expr { ( $h * 60 + $m ) * 60 + $s }]
4047 return [expr { $seconds + $tod }]
4051 #----------------------------------------------------------------------
4053 # GetLocaleEra --
4055 # Given local time expressed in seconds from the Posix epoch,
4056 # determine localized era and year within the era.
4058 # Parameters:
4059 # date - Dictionary that must contain the keys, 'localSeconds',
4060 # whose value is expressed as the appropriate local time;
4061 # and 'year', whose value is the Gregorian year.
4062 # etable - Value of the LOCALE_ERAS key in the message catalogue
4063 # for the target locale.
4065 # Results:
4066 # Returns the dictionary, augmented with the keys, 'localeEra'
4067 # and 'localeYear'.
4069 #----------------------------------------------------------------------
4071 proc ::tcl::clock::GetLocaleEra { date etable } {
4073 set index [BSearch $etable [dict get $date localSeconds]]
4074 if { $index < 0} {
4075 dict set date localeEra \
4076 [::format %02d [expr { [dict get $date year] / 100 }]]
4077 dict set date localeYear \
4078 [expr { [dict get $date year] % 100 }]
4079 } else {
4080 dict set date localeEra [lindex $etable $index 1]
4081 dict set date localeYear [expr { [dict get $date year]
4082 - [lindex $etable $index 2] }]
4084 return $date
4088 #----------------------------------------------------------------------
4090 # GetJulianDayFromEraYearDay --
4092 # Given a year, month and day on the Gregorian calendar, determines
4093 # the Julian Day Number beginning at noon on that date.
4095 # Parameters:
4096 # date -- A dictionary in which the 'era', 'year', and
4097 # 'dayOfYear' slots are populated. The calendar in use
4098 # is determined by the date itself relative to:
4099 # changeover -- Julian day on which the Gregorian calendar was
4100 # adopted in the current locale.
4102 # Results:
4103 # Returns the given dictionary augmented with a 'julianDay' key
4104 # whose value is the desired Julian Day Number, and a 'gregorian'
4105 # key that specifies whether the calendar is Gregorian (1) or
4106 # Julian (0).
4108 # Side effects:
4109 # None.
4111 # Bugs:
4112 # This code needs to be moved to the C layer.
4114 #----------------------------------------------------------------------
4116 proc ::tcl::clock::GetJulianDayFromEraYearDay {date changeover} {
4118 # Get absolute year number from the civil year
4120 switch -exact -- [dict get $date era] {
4121 BCE {
4122 set year [expr { 1 - [dict get $date year] }]
4124 CE {
4125 set year [dict get $date year]
4128 set ym1 [expr { $year - 1 }]
4130 # Try the Gregorian calendar first.
4132 dict set date gregorian 1
4133 set jd [expr { 1721425
4134 + [dict get $date dayOfYear]
4135 + ( 365 * $ym1 )
4136 + ( $ym1 / 4 )
4137 - ( $ym1 / 100 )
4138 + ( $ym1 / 400 ) }]
4140 # If the date is before the Gregorian change, use the Julian calendar.
4142 if { $jd < $changeover } {
4143 dict set date gregorian 0
4144 set jd [expr { 1721423
4145 + [dict get $date dayOfYear]
4146 + ( 365 * $ym1 )
4147 + ( $ym1 / 4 ) }]
4150 dict set date julianDay $jd
4151 return $date
4154 #----------------------------------------------------------------------
4156 # GetJulianDayFromEraYearMonthWeekDay --
4158 # Determines the Julian Day number corresponding to the nth
4159 # given day-of-the-week in a given month.
4161 # Parameters:
4162 # date - Dictionary containing the keys, 'era', 'year', 'month'
4163 # 'weekOfMonth', 'dayOfWeek', and 'dayOfWeekInMonth'.
4164 # changeover - Julian Day of adoption of the Gregorian calendar
4166 # Results:
4167 # Returns the given dictionary, augmented with a 'julianDay' key.
4169 # Side effects:
4170 # None.
4172 # Bugs:
4173 # This code needs to be moved to the C layer.
4175 #----------------------------------------------------------------------
4177 proc ::tcl::clock::GetJulianDayFromEraYearMonthWeekDay {date changeover} {
4179 # Come up with a reference day; either the zeroeth day of the
4180 # given month (dayOfWeekInMonth >= 0) or the seventh day of the
4181 # following month (dayOfWeekInMonth < 0)
4183 set date2 $date
4184 set week [dict get $date dayOfWeekInMonth]
4185 if { $week >= 0 } {
4186 dict set date2 dayOfMonth 0
4187 } else {
4188 dict incr date2 month
4189 dict set date2 dayOfMonth 7
4191 set date2 [GetJulianDayFromEraYearMonthDay $date2[set date2 {}] \
4192 $changeover]
4193 set wd0 [WeekdayOnOrBefore [dict get $date dayOfWeek] \
4194 [dict get $date2 julianDay]]
4195 dict set date julianDay [expr { $wd0 + 7 * $week }]
4196 return $date
4200 #----------------------------------------------------------------------
4202 # IsGregorianLeapYear --
4204 # Determines whether a given date represents a leap year in the
4205 # Gregorian calendar.
4207 # Parameters:
4208 # date -- The date to test. The fields, 'era', 'year' and 'gregorian'
4209 # must be set.
4211 # Results:
4212 # Returns 1 if the year is a leap year, 0 otherwise.
4214 # Side effects:
4215 # None.
4217 #----------------------------------------------------------------------
4219 proc ::tcl::clock::IsGregorianLeapYear { date } {
4221 switch -exact -- [dict get $date era] {
4222 BCE {
4223 set year [expr { 1 - [dict get $date year]}]
4225 CE {
4226 set year [dict get $date year]
4229 if { $year % 4 != 0 } {
4230 return 0
4231 } elseif { ![dict get $date gregorian] } {
4232 return 1
4233 } elseif { $year % 400 == 0 } {
4234 return 1
4235 } elseif { $year % 100 == 0 } {
4236 return 0
4237 } else {
4238 return 1
4243 #----------------------------------------------------------------------
4245 # WeekdayOnOrBefore --
4247 # Determine the nearest day of week (given by the 'weekday'
4248 # parameter, Sunday==0) on or before a given Julian Day.
4250 # Parameters:
4251 # weekday -- Day of the week
4252 # j -- Julian Day number
4254 # Results:
4255 # Returns the Julian Day Number of the desired date.
4257 # Side effects:
4258 # None.
4260 #----------------------------------------------------------------------
4262 proc ::tcl::clock::WeekdayOnOrBefore { weekday j } {
4264 set k [expr { ( $weekday + 6 ) % 7 }]
4265 return [expr { $j - ( $j - $k ) % 7 }]
4269 #----------------------------------------------------------------------
4271 # BSearch --
4273 # Service procedure that does binary search in several places
4274 # inside the 'clock' command.
4276 # Parameters:
4277 # list - List of lists, sorted in ascending order by the
4278 # first elements
4279 # key - Value to search for
4281 # Results:
4282 # Returns the index of the greatest element in $list that is less
4283 # than or equal to $key.
4285 # Side effects:
4286 # None.
4288 #----------------------------------------------------------------------
4290 proc ::tcl::clock::BSearch { list key } {
4292 if {[llength $list] == 0} {
4293 return -1
4295 if { $key < [lindex $list 0 0] } {
4296 return -1
4299 set l 0
4300 set u [expr { [llength $list] - 1 }]
4302 while { $l < $u } {
4304 # At this point, we know that
4305 # $k >= [lindex $list $l 0]
4306 # Either $u == [llength $list] or else $k < [lindex $list $u+1 0]
4307 # We find the midpoint of the interval {l,u} rounded UP, compare
4308 # against it, and set l or u to maintain the invariant. Note
4309 # that the interval shrinks at each step, guaranteeing convergence.
4311 set m [expr { ( $l + $u + 1 ) / 2 }]
4312 if { $key >= [lindex $list $m 0] } {
4313 set l $m
4314 } else {
4315 set u [expr { $m - 1 }]
4319 return $l
4322 #----------------------------------------------------------------------
4324 # clock add --
4326 # Adds an offset to a given time.
4328 # Syntax:
4329 # clock add clockval ?count unit?... ?-option value?
4331 # Parameters:
4332 # clockval -- Starting time value
4333 # count -- Amount of a unit of time to add
4334 # unit -- Unit of time to add, must be one of:
4335 # years year months month weeks week
4336 # days day hours hour minutes minute
4337 # seconds second
4339 # Options:
4340 # -gmt BOOLEAN
4341 # (Deprecated) Flag synonymous with '-timezone :GMT'
4342 # -timezone ZONE
4343 # Name of the time zone in which calculations are to be done.
4344 # -locale NAME
4345 # Name of the locale in which calculations are to be done.
4346 # Used to determine the Gregorian change date.
4348 # Results:
4349 # Returns the given time adjusted by the given offset(s) in
4350 # order.
4352 # Notes:
4353 # It is possible that adding a number of months or years will adjust
4354 # the day of the month as well. For instance, the time at
4355 # one month after 31 January is either 28 or 29 February, because
4356 # February has fewer than 31 days.
4358 #----------------------------------------------------------------------
4360 proc ::tcl::clock::add { clockval args } {
4362 if { [llength $args] % 2 != 0 } {
4363 set cmdName "clock add"
4364 return -code error \
4365 -errorcode [list CLOCK wrongNumArgs] \
4366 "wrong \# args: should be\
4367 \"$cmdName clockval ?number units?...\
4368 ?-gmt boolean? ?-locale LOCALE? ?-timezone ZONE?\""
4370 if { [catch { expr {wide($clockval)} } result] } {
4371 return -code error $result
4374 set offsets {}
4375 set gmt 0
4376 set locale c
4377 set timezone [GetSystemTimeZone]
4379 foreach { a b } $args {
4381 if { [string is integer -strict $a] } {
4383 lappend offsets $a $b
4385 } else {
4387 switch -exact -- $a {
4389 -g - -gm - -gmt {
4390 set gmt $b
4392 -l - -lo - -loc - -loca - -local - -locale {
4393 set locale [string tolower $b]
4395 -t - -ti - -tim - -time - -timez - -timezo - -timezon -
4396 -timezone {
4397 set timezone $b
4399 default {
4400 return -code error \
4401 -errorcode [list CLOCK badSwitch $a] \
4402 "bad switch \"$a\",\
4403 must be -gmt, -locale or -timezone"
4409 # Check options for validity
4411 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
4412 return -code error \
4413 -errorcode [list CLOCK gmtWithTimezone] \
4414 "cannot use -gmt and -timezone in same call"
4416 if { [catch { expr { wide($clockval) } } result] } {
4417 return -code error \
4418 "expected integer but got \"$clockval\""
4420 if { ![string is boolean $gmt] } {
4421 return -code error \
4422 "expected boolean value but got \"$gmt\""
4423 } else {
4424 if { $gmt } {
4425 set timezone :GMT
4429 EnterLocale $locale oldLocale
4431 set changeover [mc GREGORIAN_CHANGE_DATE]
4433 if {[catch {SetupTimeZone $timezone} retval opts]} {
4434 dict unset opts -errorinfo
4435 return -options $opts $retval
4438 set status [catch {
4440 foreach { quantity unit } $offsets {
4442 switch -exact -- $unit {
4444 years - year {
4445 set clockval \
4446 [AddMonths [expr { 12 * $quantity }] \
4447 $clockval $timezone $changeover]
4449 months - month {
4450 set clockval [AddMonths $quantity $clockval $timezone \
4451 $changeover]
4454 weeks - week {
4455 set clockval [AddDays [expr { 7 * $quantity }] \
4456 $clockval $timezone $changeover]
4458 days - day {
4459 set clockval [AddDays $quantity $clockval $timezone \
4460 $changeover]
4463 hours - hour {
4464 set clockval [expr { 3600 * $quantity + $clockval }]
4466 minutes - minute {
4467 set clockval [expr { 60 * $quantity + $clockval }]
4469 seconds - second {
4470 set clockval [expr { $quantity + $clockval }]
4473 default {
4474 error "unknown unit \"$unit\", must be \
4475 years, months, weeks, days, hours, minutes or seconds" \
4476 "unknown unit \"$unit\", must be \
4477 years, months, weeks, days, hours, minutes or seconds" \
4478 [list CLOCK badUnit $unit]
4482 } result opts]
4484 # Restore the locale
4486 if { [info exists oldLocale] } {
4487 mclocale $oldLocale
4490 if { $status == 1 } {
4491 if { [lindex [dict get $opts -errorcode] 0] eq {CLOCK} } {
4492 dict unset opts -errorinfo
4494 return -options $opts $result
4495 } else {
4496 return $clockval
4501 #----------------------------------------------------------------------
4503 # AddMonths --
4505 # Add a given number of months to a given clock value in a given
4506 # time zone.
4508 # Parameters:
4509 # months - Number of months to add (may be negative)
4510 # clockval - Seconds since the epoch before the operation
4511 # timezone - Time zone in which the operation is to be performed
4513 # Results:
4514 # Returns the new clock value as a number of seconds since
4515 # the epoch.
4517 # Side effects:
4518 # None.
4520 #----------------------------------------------------------------------
4522 proc ::tcl::clock::AddMonths { months clockval timezone changeover } {
4524 variable DaysInRomanMonthInCommonYear
4525 variable DaysInRomanMonthInLeapYear
4526 variable TZData
4528 # Convert the time to year, month, day, and fraction of day.
4530 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4531 dict set date secondOfDay [expr { [dict get $date localSeconds]
4532 % 86400 }]
4533 dict set date tzName $timezone
4535 # Add the requisite number of months
4537 set m [dict get $date month]
4538 incr m $months
4539 incr m -1
4540 set delta [expr { $m / 12 }]
4541 set mm [expr { $m % 12 }]
4542 dict set date month [expr { $mm + 1 }]
4543 dict incr date year $delta
4545 # If the date doesn't exist in the current month, repair it
4547 if { [IsGregorianLeapYear $date] } {
4548 set hath [lindex $DaysInRomanMonthInLeapYear $mm]
4549 } else {
4550 set hath [lindex $DaysInRomanMonthInCommonYear $mm]
4552 if { [dict get $date dayOfMonth] > $hath } {
4553 dict set date dayOfMonth $hath
4556 # Reconvert to a number of seconds
4558 set date [GetJulianDayFromEraYearMonthDay \
4559 $date[set date {}]\
4560 $changeover]
4561 dict set date localSeconds \
4562 [expr { -210866803200
4563 + ( 86400 * wide([dict get $date julianDay]) )
4564 + [dict get $date secondOfDay] }]
4565 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4566 $changeover]
4568 return [dict get $date seconds]
4572 #----------------------------------------------------------------------
4574 # AddDays --
4576 # Add a given number of days to a given clock value in a given
4577 # time zone.
4579 # Parameters:
4580 # days - Number of days to add (may be negative)
4581 # clockval - Seconds since the epoch before the operation
4582 # timezone - Time zone in which the operation is to be performed
4583 # changeover - Julian Day on which the Gregorian calendar was adopted
4584 # in the target locale.
4586 # Results:
4587 # Returns the new clock value as a number of seconds since
4588 # the epoch.
4590 # Side effects:
4591 # None.
4593 #----------------------------------------------------------------------
4595 proc ::tcl::clock::AddDays { days clockval timezone changeover } {
4597 variable TZData
4599 # Convert the time to Julian Day
4601 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4602 dict set date secondOfDay [expr { [dict get $date localSeconds]
4603 % 86400 }]
4604 dict set date tzName $timezone
4606 # Add the requisite number of days
4608 dict incr date julianDay $days
4610 # Reconvert to a number of seconds
4612 dict set date localSeconds \
4613 [expr { -210866803200
4614 + ( 86400 * wide([dict get $date julianDay]) )
4615 + [dict get $date secondOfDay] }]
4616 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4617 $changeover]
4619 return [dict get $date seconds]
4623 #----------------------------------------------------------------------
4625 # mc --
4627 # Wrapper around ::msgcat::mc that caches the result according
4628 # to the locale.
4630 # Parameters:
4631 # Accepts the name of the message to retrieve.
4633 # Results:
4634 # Returns the message text.
4636 # Side effects:
4637 # Caches the message text.
4639 # Notes:
4640 # Only the single-argument version of [mc] is supported.
4642 #----------------------------------------------------------------------
4644 proc ::tcl::clock::mc { name } {
4645 variable McLoaded
4646 set Locale [mclocale]
4647 if { [dict exists $McLoaded $Locale $name] } {
4648 return [dict get $McLoaded $Locale $name]
4649 } else {
4650 set val [::msgcat::mc $name]
4651 dict set McLoaded $Locale $name $val
4652 return $val
4656 #----------------------------------------------------------------------
4658 # ClearCaches --
4660 # Clears all caches to reclaim the memory used in [clock]
4662 # Parameters:
4663 # None.
4665 # Results:
4666 # None.
4668 # Side effects:
4669 # Caches are cleared.
4671 #----------------------------------------------------------------------
4673 proc ::tcl::clock::ClearCaches {} {
4675 variable FormatProc
4676 variable LocaleNumeralCache
4677 variable McLoaded
4678 variable CachedSystemTimeZone
4679 variable TimeZoneBad
4681 foreach p [info procs [namespace current]::scanproc'*] {
4682 rename $p {}
4684 foreach p [info procs [namespace current]::formatproc'*] {
4685 rename $p {}
4688 catch {unset FormatProc}
4689 set LocaleNumeralCache {}
4690 set McLoaded {}
4691 catch {unset CachedSystemTimeZone}
4692 set TimeZoneBad {}
4693 InitTZData