1 #----------------------------------------------------------------------
5 # This file implements the portions of the [clock] ensemble that are
6 # coded in Tcl. Refer to the users' manual to see the description of
7 # the [clock] command and its subcommands.
10 #----------------------------------------------------------------------
12 # Copyright (c) 2004-2007 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 we need
19 # access to the Registry on Windows systems.
22 package require msgcat 1.6
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 so that the
31 # library code can find message catalogs and time zone definition files.
33 namespace eval ::tcl::clock \
34 [list variable LibDir [file dirname [info script]]]
36 #----------------------------------------------------------------------
42 # The 'clock' command manipulates time. Refer to the user documentation for
43 # the available subcommands and what they do.
45 #----------------------------------------------------------------------
47 namespace eval ::tcl::clock {
49 # Export the subcommands
51 namespace export format
52 namespace export clicks
53 namespace export microseconds
54 namespace export milliseconds
56 namespace export seconds
59 # Import the message catalog commands that we use.
61 namespace import ::msgcat::mcload
62 namespace import ::msgcat::mclocale
63 namespace import ::msgcat::mc
64 namespace import ::msgcat::mcpackagelocale
68 #----------------------------------------------------------------------
70 # ::tcl::clock::Initialize --
72 # Finish initializing the 'clock' subsystem
78 # Namespace variable in the 'clock' subsystem are initialized.
80 # The '::tcl::clock::Initialize' procedure initializes the namespace variables
81 # and root locale message catalog for the 'clock' subsystem. It is broken
82 # into a procedure rather than simply evaluated as a script so that it will be
83 # able to use local variables, avoiding the dangers of 'creative writing' as
86 #----------------------------------------------------------------------
88 proc ::tcl::clock::Initialize {} {
90 rename ::tcl::clock::Initialize {}
94 # Define the Greenwich time zone
99 set TZData(:Etc/GMT) {
100 {-9223372036854775808 0 0 GMT}
102 set TZData(:GMT) $TZData(:Etc/GMT)
103 set TZData(:Etc/UTC) {
104 {-9223372036854775808 0 0 UTC}
106 set TZData(:UTC) $TZData(:Etc/UTC)
107 set TZData(:localtime) {}
111 mcpackagelocale set {}
112 ::msgcat::mcpackageconfig set mcfolder [file join $LibDir msgs]
113 ::msgcat::mcpackageconfig set unknowncmd ""
114 ::msgcat::mcpackageconfig set changecmd ChangeCurrentLocale
116 # Define the message catalog for the root locale.
118 ::msgcat::mcmset {} {
122 DATE_FORMAT {%m/%d/%Y}
123 DATE_TIME_FORMAT {%a %b %e %H:%M:%S %Y}
124 DAYS_OF_WEEK_ABBREV {
125 Sun Mon Tue Wed Thu Fri Sat
128 Sunday Monday Tuesday Wednesday Thursday Friday Saturday
130 GREGORIAN_CHANGE_DATE 2299161
131 LOCALE_DATE_FORMAT {%m/%d/%Y}
132 LOCALE_DATE_TIME_FORMAT {%a %b %e %H:%M:%S %Y}
135 00 01 02 03 04 05 06 07 08 09
136 10 11 12 13 14 15 16 17 18 19
137 20 21 22 23 24 25 26 27 28 29
138 30 31 32 33 34 35 36 37 38 39
139 40 41 42 43 44 45 46 47 48 49
140 50 51 52 53 54 55 56 57 58 59
141 60 61 62 63 64 65 66 67 68 69
142 70 71 72 73 74 75 76 77 78 79
143 80 81 82 83 84 85 86 87 88 89
144 90 91 92 93 94 95 96 97 98 99
146 LOCALE_TIME_FORMAT {%H:%M:%S}
147 LOCALE_YEAR_FORMAT {%EC%Ey}
149 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
152 January February March
154 July August September
155 October November December
158 TIME_FORMAT {%H:%M:%S}
159 TIME_FORMAT_12 {%I:%M:%S %P}
160 TIME_FORMAT_24 {%H:%M}
161 TIME_FORMAT_24_SECS {%H:%M:%S}
164 # Define a few Gregorian change dates for other locales. In most cases
165 # the change date follows a language, because a nation's colonies changed
166 # at the same time as the nation itself. In many cases, different
167 # national boundaries existed; the dominating rule is to follow the
170 # Italy, Spain, Portugal, Poland
172 ::msgcat::mcset it GREGORIAN_CHANGE_DATE 2299161
173 ::msgcat::mcset es GREGORIAN_CHANGE_DATE 2299161
174 ::msgcat::mcset pt GREGORIAN_CHANGE_DATE 2299161
175 ::msgcat::mcset pl GREGORIAN_CHANGE_DATE 2299161
179 ::msgcat::mcset fr GREGORIAN_CHANGE_DATE 2299227
181 # For Belgium, we follow Southern Netherlands; Liege Diocese changed
182 # several weeks later.
184 ::msgcat::mcset fr_BE GREGORIAN_CHANGE_DATE 2299238
185 ::msgcat::mcset nl_BE GREGORIAN_CHANGE_DATE 2299238
189 ::msgcat::mcset de_AT GREGORIAN_CHANGE_DATE 2299527
193 ::msgcat::mcset hu GREGORIAN_CHANGE_DATE 2301004
195 # Germany, Norway, Denmark (Catholic Germany changed earlier)
197 ::msgcat::mcset de_DE GREGORIAN_CHANGE_DATE 2342032
198 ::msgcat::mcset nb GREGORIAN_CHANGE_DATE 2342032
199 ::msgcat::mcset nn GREGORIAN_CHANGE_DATE 2342032
200 ::msgcat::mcset no GREGORIAN_CHANGE_DATE 2342032
201 ::msgcat::mcset da GREGORIAN_CHANGE_DATE 2342032
203 # Holland (Brabant, Gelderland, Flanders, Friesland, etc. changed at
206 ::msgcat::mcset nl GREGORIAN_CHANGE_DATE 2342165
208 # Protestant Switzerland (Catholic cantons changed earlier)
210 ::msgcat::mcset fr_CH GREGORIAN_CHANGE_DATE 2361342
211 ::msgcat::mcset it_CH GREGORIAN_CHANGE_DATE 2361342
212 ::msgcat::mcset de_CH GREGORIAN_CHANGE_DATE 2361342
214 # English speaking countries
216 ::msgcat::mcset en GREGORIAN_CHANGE_DATE 2361222
218 # Sweden (had several changes onto and off of the Gregorian calendar)
220 ::msgcat::mcset sv GREGORIAN_CHANGE_DATE 2361390
224 ::msgcat::mcset ru GREGORIAN_CHANGE_DATE 2421639
226 # Romania (Transylvania changed earler - perhaps de_RO should show the
229 ::msgcat::mcset ro GREGORIAN_CHANGE_DATE 2422063
233 ::msgcat::mcset el GREGORIAN_CHANGE_DATE 2423480
235 #------------------------------------------------------------------
239 #------------------------------------------------------------------
241 # Paths at which binary time zone data for the Olson libraries are known
242 # to reside on various operating systems
244 variable ZoneinfoPaths {}
247 /usr/share/lib/zoneinfo
249 /usr/local/etc/zoneinfo
251 if { [file isdirectory $path] } {
252 lappend ZoneinfoPaths $path
256 # Define the directories for time zone data and message catalogs.
258 variable DataDir [file join $LibDir tzdata]
260 # Number of days in the months, in common years and leap years.
262 variable DaysInRomanMonthInCommonYear \
263 { 31 28 31 30 31 30 31 31 30 31 30 31 }
264 variable DaysInRomanMonthInLeapYear \
265 { 31 29 31 30 31 30 31 31 30 31 30 31 }
266 variable DaysInPriorMonthsInCommonYear [list 0]
267 variable DaysInPriorMonthsInLeapYear [list 0]
269 foreach j $DaysInRomanMonthInCommonYear {
270 lappend DaysInPriorMonthsInCommonYear [incr i $j]
273 foreach j $DaysInRomanMonthInLeapYear {
274 lappend DaysInPriorMonthsInLeapYear [incr i $j]
277 # Another epoch (Hi, Jeff!)
279 variable Roddenberry 1946
283 variable MINWIDE -9223372036854775808
284 variable MAXWIDE 9223372036854775807
286 # Day before Leap Day
290 # Translation table to map Windows TZI onto cities, so that the Olson
291 # rules can apply. In some cases the mapping is ambiguous, so it's wise
292 # to specify $::env(TCL_TZ) rather than simply depending on the system
295 # The keys are long lists of values obtained from the time zone
296 # information in the Registry. In order, the list elements are:
297 # Bias StandardBias DaylightBias
298 # StandardDate.wYear StandardDate.wMonth StandardDate.wDayOfWeek
299 # StandardDate.wDay StandardDate.wHour StandardDate.wMinute
300 # StandardDate.wSecond StandardDate.wMilliseconds
301 # DaylightDate.wYear DaylightDate.wMonth DaylightDate.wDayOfWeek
302 # DaylightDate.wDay DaylightDate.wHour DaylightDate.wMinute
303 # DaylightDate.wSecond DaylightDate.wMilliseconds
304 # The values are the names of time zones where those rules apply. There
305 # is considerable ambiguity in certain zones; an attempt has been made to
306 # make a reasonable guess, but this table needs to be taken with a grain
309 variable WinZoneInfo [dict create {*}{
310 {-43200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Kwajalein
311 {-39600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Midway
312 {-36000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Honolulu
313 {-32400 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Anchorage
314 {-28800 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Los_Angeles
315 {-28800 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Tijuana
316 {-25200 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Denver
317 {-25200 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Chihuahua
318 {-25200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Phoenix
319 {-21600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Regina
320 {-21600 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Chicago
321 {-21600 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Mexico_City
322 {-18000 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/New_York
323 {-18000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Indianapolis
324 {-14400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Caracas
325 {-14400 0 3600 0 3 6 2 23 59 59 999 0 10 6 2 23 59 59 999}
327 {-14400 0 3600 0 2 0 5 2 0 0 0 0 11 0 1 2 0 0 0} :America/Manaus
328 {-14400 0 3600 0 11 0 1 2 0 0 0 0 3 0 2 2 0 0 0} :America/Halifax
329 {-12600 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/St_Johns
330 {-10800 0 3600 0 2 0 2 2 0 0 0 0 10 0 3 2 0 0 0} :America/Sao_Paulo
331 {-10800 0 3600 0 10 0 5 2 0 0 0 0 4 0 1 2 0 0 0} :America/Godthab
332 {-10800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :America/Buenos_Aires
333 {-10800 0 3600 0 2 0 5 2 0 0 0 0 11 0 1 2 0 0 0} :America/Bahia
334 {-10800 0 3600 0 3 0 2 2 0 0 0 0 10 0 1 2 0 0 0} :America/Montevideo
335 {-7200 0 3600 0 9 0 5 2 0 0 0 0 3 0 5 2 0 0 0} :America/Noronha
336 {-3600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Atlantic/Azores
337 {-3600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Atlantic/Cape_Verde
338 {0 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :UTC
339 {0 0 3600 0 10 0 5 2 0 0 0 0 3 0 5 1 0 0 0} :Europe/London
340 {3600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Africa/Kinshasa
341 {3600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :CET
342 {7200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Africa/Harare
343 {7200 0 3600 0 9 4 5 23 59 59 0 0 4 4 5 23 59 59 0}
345 {7200 0 3600 0 10 0 5 4 0 0 0 0 3 0 5 3 0 0 0} :Europe/Helsinki
346 {7200 0 3600 0 9 0 3 2 0 0 0 0 3 5 5 2 0 0 0} :Asia/Jerusalem
347 {7200 0 3600 0 9 0 5 1 0 0 0 0 3 0 5 0 0 0 0} :Europe/Bucharest
348 {7200 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Europe/Athens
349 {7200 0 3600 0 9 5 5 1 0 0 0 0 3 4 5 0 0 0 0} :Asia/Amman
350 {7200 0 3600 0 10 6 5 23 59 59 999 0 3 0 5 0 0 0 0}
352 {7200 0 -3600 0 4 0 1 2 0 0 0 0 9 0 1 2 0 0 0} :Africa/Windhoek
353 {10800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Riyadh
354 {10800 0 3600 0 10 0 1 4 0 0 0 0 4 0 1 3 0 0 0} :Asia/Baghdad
355 {10800 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Europe/Moscow
356 {12600 0 3600 0 9 2 4 2 0 0 0 0 3 0 1 2 0 0 0} :Asia/Tehran
357 {14400 0 3600 0 10 0 5 5 0 0 0 0 3 0 5 4 0 0 0} :Asia/Baku
358 {14400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Muscat
359 {14400 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Tbilisi
360 {16200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Kabul
361 {18000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Karachi
362 {18000 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Yekaterinburg
363 {19800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Calcutta
364 {20700 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Katmandu
365 {21600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Dhaka
366 {21600 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Novosibirsk
367 {23400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Rangoon
368 {25200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Bangkok
369 {25200 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Krasnoyarsk
370 {28800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Chongqing
371 {28800 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Irkutsk
372 {32400 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Asia/Tokyo
373 {32400 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Yakutsk
374 {34200 0 3600 0 3 0 5 3 0 0 0 0 10 0 5 2 0 0 0} :Australia/Adelaide
375 {34200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Australia/Darwin
376 {36000 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Australia/Brisbane
377 {36000 0 3600 0 10 0 5 3 0 0 0 0 3 0 5 2 0 0 0} :Asia/Vladivostok
378 {36000 0 3600 0 3 0 5 3 0 0 0 0 10 0 1 2 0 0 0} :Australia/Hobart
379 {36000 0 3600 0 3 0 5 3 0 0 0 0 10 0 5 2 0 0 0} :Australia/Sydney
380 {39600 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Noumea
381 {43200 0 3600 0 3 0 3 3 0 0 0 0 10 0 1 2 0 0 0} :Pacific/Auckland
382 {43200 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Fiji
383 {46800 0 3600 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0} :Pacific/Tongatapu
386 # Groups of fields that specify the date, priorities, and code bursts that
387 # determine Julian Day Number given those groups. The code in [clock
388 # scan] will choose the highest priority (lowest numbered) set of fields
389 # that determines the date.
391 variable DateParseActions {
397 { era century yearOfCentury month dayOfMonth } 2 {
398 dict set date year [expr { 100 * [dict get $date century]
399 + [dict get $date yearOfCentury] }]
400 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
403 { era century yearOfCentury dayOfYear } 2 {
404 dict set date year [expr { 100 * [dict get $date century]
405 + [dict get $date yearOfCentury] }]
406 set date [GetJulianDayFromEraYearDay $date[set date {}] \
410 { century yearOfCentury month dayOfMonth } 3 {
412 dict set date year [expr { 100 * [dict get $date century]
413 + [dict get $date yearOfCentury] }]
414 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
417 { century yearOfCentury dayOfYear } 3 {
419 dict set date year [expr { 100 * [dict get $date century]
420 + [dict get $date yearOfCentury] }]
421 set date [GetJulianDayFromEraYearDay $date[set date {}] \
424 { iso8601Century iso8601YearOfCentury iso8601Week dayOfWeek } 3 {
426 dict set date iso8601Year \
427 [expr { 100 * [dict get $date iso8601Century]
428 + [dict get $date iso8601YearOfCentury] }]
429 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
433 { yearOfCentury month dayOfMonth } 4 {
434 set date [InterpretTwoDigitYear $date[set date {}] $baseTime]
436 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
439 { yearOfCentury dayOfYear } 4 {
440 set date [InterpretTwoDigitYear $date[set date {}] $baseTime]
442 set date [GetJulianDayFromEraYearDay $date[set date {}] \
445 { iso8601YearOfCentury iso8601Week dayOfWeek } 4 {
446 set date [InterpretTwoDigitYear \
447 $date[set date {}] $baseTime \
448 iso8601YearOfCentury iso8601Year]
450 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
454 { month dayOfMonth } 5 {
455 set date [AssignBaseYear $date[set date {}] \
456 $baseTime $timeZone $changeover]
457 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
461 set date [AssignBaseYear $date[set date {}] \
462 $baseTime $timeZone $changeover]
463 set date [GetJulianDayFromEraYearDay $date[set date {}] \
466 { iso8601Week dayOfWeek } 5 {
467 set date [AssignBaseIso8601Year $date[set date {}] \
468 $baseTime $timeZone $changeover]
469 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
474 set date [AssignBaseMonth $date[set date {}] \
475 $baseTime $timeZone $changeover]
476 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] \
481 set date [AssignBaseWeek $date[set date {}] \
482 $baseTime $timeZone $changeover]
483 set date [GetJulianDayFromEraYearWeekDay $date[set date {}] \
488 set date [AssignBaseJulianDay $date[set date {}] \
489 $baseTime $timeZone $changeover]
493 # Groups of fields that specify time of day, priorities, and code that
496 variable TimeParseActions {
500 { hourAMPM minute second amPmIndicator } 2 {
501 dict set date secondOfDay [InterpretHMSP $date]
503 { hour minute second } 2 {
504 dict set date secondOfDay [InterpretHMS $date]
507 { hourAMPM minute amPmIndicator } 3 {
508 dict set date second 0
509 dict set date secondOfDay [InterpretHMSP $date]
512 dict set date second 0
513 dict set date secondOfDay [InterpretHMS $date]
516 { hourAMPM amPmIndicator } 4 {
517 dict set date minute 0
518 dict set date second 0
519 dict set date secondOfDay [InterpretHMSP $date]
522 dict set date minute 0
523 dict set date second 0
524 dict set date secondOfDay [InterpretHMS $date]
528 dict set date secondOfDay 0
532 # Legacy time zones, used primarily for parsing RFC822 dates.
534 variable LegacyTimeZone [dict create \
626 variable LocaleNumeralCache {}; # Dictionary whose keys are locale
627 # names and whose values are pairs
628 # comprising regexes matching numerals
629 # in the given locales and dictionaries
630 # mapping the numerals to their numeric
632 # variable CachedSystemTimeZone; # If 'CachedSystemTimeZone' exists,
633 # it contains the value of the
634 # system time zone, as determined from
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
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 #----------------------------------------------------------------------
655 # Formats a count of seconds since the Posix Epoch as a time of day.
657 # The 'clock format' command formats times of day for output. Refer to the
658 # user documentation to see what it does.
660 #----------------------------------------------------------------------
662 proc ::tcl::clock::format { args } {
667 lassign [ParseFormatArgs {*}$args] format locale timezone
668 set locale [string tolower $locale]
669 set clockval [lindex $args 0]
671 # Get the data for time changes in the given zone
673 if {$timezone eq ""} {
674 set timezone [GetSystemTimeZone]
676 if {![info exists TZData($timezone)]} {
677 if {[catch {SetupTimeZone $timezone} retval opts]} {
678 dict unset opts -errorinfo
679 return -options $opts $retval
683 # Build a procedure to format the result. Cache the built procedure's name
684 # in the 'FormatProc' array to avoid losing its internal representation,
685 # which contains the name resolution.
687 set procName formatproc'$format'$locale
688 set procName [namespace current]::[string map {: {\:} \\ {\\}} $procName]
689 if {[info exists FormatProc($procName)]} {
690 set procName $FormatProc($procName)
692 set FormatProc($procName) \
693 [ParseClockFormatFormat $procName $format $locale]
696 return [$procName $clockval $timezone]
700 #----------------------------------------------------------------------
702 # ParseClockFormatFormat --
704 # Builds and caches a procedure that formats a time value.
707 # format -- Format string to use
708 # locale -- Locale in which the format string is to be interpreted
711 # Returns the name of the newly-built procedure.
713 #----------------------------------------------------------------------
715 proc ::tcl::clock::ParseClockFormatFormat {procName format locale} {
717 if {[namespace which $procName] ne {}} {
721 # Map away the locale-dependent composite format groups
725 # Change locale if a fresh locale has been given on the command line.
728 return [ParseClockFormatFormat2 $format $locale $procName]
729 } trap CLOCK {result opts} {
730 dict unset opts -errorinfo
731 return -options $opts $result
735 proc ::tcl::clock::ParseClockFormatFormat2 {format locale procName} {
737 set didLocaleNumerals 0
739 [string map [list @GREGORIAN_CHANGE_DATE@ \
740 [mc GREGORIAN_CHANGE_DATE]] \
743 set date [GetDateFields $clockval \
745 @GREGORIAN_CHANGE_DATE@]
751 set format [LocalizeFormat $locale $format]
753 foreach char [split $format {}] {
754 switch -exact -- $state {
756 if { [string equal % $char] } {
759 append formatString $char
762 percent { # Character following a '%' character
764 switch -exact -- $char {
765 % { # A literal character, '%'
766 append formatString %%
768 a { # Day of week, abbreviated
769 append formatString %s
770 append substituents \
772 [list @DAYS_OF_WEEK_ABBREV@ \
773 [list [mc DAYS_OF_WEEK_ABBREV]]] \
774 { [lindex @DAYS_OF_WEEK_ABBREV@ \
775 [expr {[dict get $date dayOfWeek] \
778 A { # Day of week, spelt out.
779 append formatString %s
780 append substituents \
782 [list @DAYS_OF_WEEK_FULL@ \
783 [list [mc DAYS_OF_WEEK_FULL]]] \
784 { [lindex @DAYS_OF_WEEK_FULL@ \
785 [expr {[dict get $date dayOfWeek] \
788 b - h { # Name of month, abbreviated.
789 append formatString %s
790 append substituents \
792 [list @MONTHS_ABBREV@ \
793 [list [mc MONTHS_ABBREV]]] \
794 { [lindex @MONTHS_ABBREV@ \
795 [expr {[dict get $date month]-1}]]}]
797 B { # Name of month, spelt out
798 append formatString %s
799 append substituents \
801 [list @MONTHS_FULL@ \
802 [list [mc MONTHS_FULL]]] \
803 { [lindex @MONTHS_FULL@ \
804 [expr {[dict get $date month]-1}]]}]
807 append formatString %02d
808 append substituents \
809 { [expr {[dict get $date year] / 100}]}
811 d { # Day of month, with leading zero
812 append formatString %02d
813 append substituents { [dict get $date dayOfMonth]}
815 e { # Day of month, without leading zero
816 append formatString %2d
817 append substituents { [dict get $date dayOfMonth]}
819 E { # Format group in a locale-dependent
822 if {!$didLocaleEra} {
823 append preFormatCode \
825 [list @LOCALE_ERAS@ \
826 [list [mc LOCALE_ERAS]]] \
828 set date [GetLocaleEra \
833 if {!$didLocaleNumerals} {
834 append preFormatCode \
835 [list set localeNumerals \
836 [mc LOCALE_NUMERALS]] \n
837 set didLocaleNumerals 1
840 g { # Two-digit year relative to ISO8601
842 append formatString %02d
843 append substituents \
844 { [expr { [dict get $date iso8601Year] % 100 }]}
846 G { # Four-digit year relative to ISO8601
848 append formatString %02d
849 append substituents { [dict get $date iso8601Year]}
851 H { # Hour in the 24-hour day, leading zero
852 append formatString %02d
853 append substituents \
854 { [expr { [dict get $date localSeconds] \
857 I { # Hour AM/PM, with leading zero
858 append formatString %02d
859 append substituents \
860 { [expr { ( ( ( [dict get $date localSeconds] \
867 j { # Day of year (001-366)
868 append formatString %03d
869 append substituents { [dict get $date dayOfYear]}
871 J { # Julian Day Number
872 append formatString %07ld
873 append substituents { [dict get $date julianDay]}
875 k { # Hour (0-23), no leading zero
876 append formatString %2d
877 append substituents \
878 { [expr { [dict get $date localSeconds]
882 l { # Hour (12-11), no leading zero
883 append formatString %2d
884 append substituents \
885 { [expr { ( ( ( [dict get $date localSeconds]
892 m { # Month number, leading zero
893 append formatString %02d
894 append substituents { [dict get $date month]}
896 M { # Minute of the hour, leading zero
897 append formatString %02d
898 append substituents \
899 { [expr { [dict get $date localSeconds]
903 n { # A literal newline
904 append formatString \n
906 N { # Month number, no leading zero
907 append formatString %2d
908 append substituents { [dict get $date month]}
910 O { # A format group in the locale's
911 # alternative numerals
913 if {!$didLocaleNumerals} {
914 append preFormatCode \
915 [list set localeNumerals \
916 [mc LOCALE_NUMERALS]] \n
917 set didLocaleNumerals 1
920 p { # Localized 'AM' or 'PM' indicator
921 # converted to uppercase
922 append formatString %s
923 append preFormatCode \
924 [list set AM [string toupper [mc AM]]] \n \
925 [list set PM [string toupper [mc PM]]] \n
926 append substituents \
927 { [expr {(([dict get $date localSeconds]
931 P { # Localized 'AM' or 'PM' indicator
932 append formatString %s
933 append preFormatCode \
934 [list set am [mc AM]] \n \
935 [list set pm [mc PM]] \n
936 append substituents \
937 { [expr {(([dict get $date localSeconds]
943 append formatString %s
944 append substituents { [FormatStarDate $date]}
946 s { # Seconds from the Posix Epoch
947 append formatString %s
948 append substituents { [dict get $date seconds]}
950 S { # Second of the minute, with
952 append formatString %02d
953 append substituents \
954 { [expr { [dict get $date localSeconds]
957 t { # A literal tab character
958 append formatString \t
960 u { # Day of the week (1-Monday, 7-Sunday)
961 append formatString %1d
962 append substituents { [dict get $date dayOfWeek]}
964 U { # Week of the year (00-53). The
965 # first Sunday of the year is the
966 # first day of week 01
967 append formatString %02d
968 append preFormatCode {
969 set dow [dict get $date dayOfWeek]
975 [expr { ( [dict get $date dayOfYear]
979 append substituents { $UweekNumber}
981 V { # The ISO8601 week number
982 append formatString %02d
983 append substituents { [dict get $date iso8601Week]}
985 w { # Day of the week (0-Sunday,
987 append formatString %1d
988 append substituents \
989 { [expr { [dict get $date dayOfWeek] % 7 }]}
991 W { # Week of the year (00-53). The first
992 # Monday of the year is the first day
994 append preFormatCode {
996 [expr { ( [dict get $date dayOfYear]
997 - [dict get $date dayOfWeek]
1001 append formatString %02d
1002 append substituents { $WweekNumber}
1004 y { # The two-digit year of the century
1005 append formatString %02d
1006 append substituents \
1007 { [expr { [dict get $date year] % 100 }]}
1009 Y { # The four-digit year
1010 append formatString %04d
1011 append substituents { [dict get $date year]}
1013 z { # The time zone as hours and minutes
1014 # east (+) or west (-) of Greenwich
1015 append formatString %s
1016 append substituents { [FormatNumericTimeZone \
1017 [dict get $date tzOffset]]}
1019 Z { # The name of the time zone
1020 append formatString %s
1021 append substituents { [dict get $date tzName]}
1023 % { # A literal percent character
1024 append formatString %%
1026 default { # An unknown escape sequence
1027 append formatString %% $char
1031 percentE { # Character following %E
1033 switch -exact -- $char {
1035 append formatString %s
1036 append substituents { } \
1038 [list @BCE@ [list [mc BCE]] \
1039 @CE@ [list [mc CE]]] \
1040 {[dict get {BCE @BCE@ CE @CE@} \
1041 [dict get $date era]]}]
1043 C { # Locale-dependent era
1044 append formatString %s
1045 append substituents { [dict get $date localeEra]}
1047 y { # Locale-dependent year of the era
1048 append preFormatCode {
1049 set y [dict get $date localeYear]
1050 if { $y >= 0 && $y < 100 } {
1051 set Eyear [lindex $localeNumerals $y]
1056 append formatString %s
1057 append substituents { $Eyear}
1059 default { # Unknown %E format group
1060 append formatString %%E $char
1064 percentO { # Character following %O
1066 switch -exact -- $char {
1067 d - e { # Day of the month in alternative
1069 append formatString %s
1070 append substituents \
1071 { [lindex $localeNumerals \
1072 [dict get $date dayOfMonth]]}
1074 H - k { # Hour of the day in alternative
1076 append formatString %s
1077 append substituents \
1078 { [lindex $localeNumerals \
1079 [expr { [dict get $date localSeconds]
1083 I - l { # Hour (12-11) AM/PM in alternative
1085 append formatString %s
1086 append substituents \
1087 { [lindex $localeNumerals \
1088 [expr { ( ( ( [dict get $date localSeconds]
1095 m { # Month number in alternative numerals
1096 append formatString %s
1097 append substituents \
1098 { [lindex $localeNumerals [dict get $date month]]}
1100 M { # Minute of the hour in alternative
1102 append formatString %s
1103 append substituents \
1104 { [lindex $localeNumerals \
1105 [expr { [dict get $date localSeconds]
1109 S { # Second of the minute in alternative
1111 append formatString %s
1112 append substituents \
1113 { [lindex $localeNumerals \
1114 [expr { [dict get $date localSeconds]
1117 u { # Day of the week (Monday=1,Sunday=7)
1118 # in alternative numerals
1119 append formatString %s
1120 append substituents \
1121 { [lindex $localeNumerals \
1122 [dict get $date dayOfWeek]]}
1124 w { # Day of the week (Sunday=0,Saturday=6)
1125 # in alternative numerals
1126 append formatString %s
1127 append substituents \
1128 { [lindex $localeNumerals \
1129 [expr { [dict get $date dayOfWeek] % 7 }]]}
1131 y { # Year of the century in alternative
1133 append formatString %s
1134 append substituents \
1135 { [lindex $localeNumerals \
1136 [expr { [dict get $date year] % 100 }]]}
1138 default { # Unknown format group
1139 append formatString %%O $char
1146 # Clean up any improperly terminated groups
1148 switch -exact -- $state {
1150 append formatString %%
1160 proc $procName {clockval timezone} "
1162 return \[::format [list $formatString] $substituents\]
1165 # puts [list $procName [info args $procName] [info body $procName]]
1170 #----------------------------------------------------------------------
1174 # Inputs a count of seconds since the Posix Epoch as a time of day.
1176 # The 'clock format' command scans times of day on input. Refer to the user
1177 # documentation to see what it does.
1179 #----------------------------------------------------------------------
1181 proc ::tcl::clock::scan { args } {
1185 # Check the count of args
1187 if { [llength $args] < 1 || [llength $args] % 2 != 1 } {
1188 set cmdName "clock scan"
1189 return -code error \
1190 -errorcode [list CLOCK wrongNumArgs] \
1191 "wrong \# args: should be\
1194 ?-format string? ?-gmt boolean?\
1195 ?-locale LOCALE? ?-timezone ZONE?\""
1200 set base [clock seconds]
1201 set string [lindex $args 0]
1205 set timezone [GetSystemTimeZone]
1207 # Pick up command line options.
1209 foreach { flag value } [lreplace $args 0 0] {
1211 switch -exact -- $flag {
1212 -b - -ba - -bas - -base {
1215 -f - -fo - -for - -form - -forma - -format {
1221 -l - -lo - -loc - -loca - -local - -locale {
1222 set locale [string tolower $value]
1224 -t - -ti - -tim - -time - -timez - -timezo - -timezon - -timezone {
1228 return -code error \
1229 -errorcode [list CLOCK badOption $flag] \
1230 "bad option \"$flag\",\
1231 must be -base, -format, -gmt, -locale or -timezone"
1236 # Check options for validity
1238 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
1239 return -code error \
1240 -errorcode [list CLOCK gmtWithTimezone] \
1241 "cannot use -gmt and -timezone in same call"
1243 if { [catch { expr { wide($base) } } result] } {
1244 return -code error "expected integer but got \"$base\""
1246 if { ![string is boolean -strict $gmt] } {
1247 return -code error "expected boolean value but got \"$gmt\""
1252 if { ![info exists saw(-format)] } {
1253 # Perhaps someday we'll localize the legacy code. Right now, it's not
1255 if { [info exists saw(-locale)] } {
1256 return -code error \
1257 -errorcode [list CLOCK flagWithLegacyFormat] \
1258 "legacy \[clock scan\] does not support -locale"
1261 return [FreeScan $string $base $timezone $locale]
1264 # Change locale if a fresh locale has been given on the command line.
1269 # Map away the locale-dependent composite format groups
1271 set scanner [ParseClockScanFormat $format $locale]
1272 return [$scanner $string $base $timezone]
1273 } trap CLOCK {result opts} {
1274 # Conceal location of generation of expected errors
1275 dict unset opts -errorinfo
1276 return -options $opts $result
1280 #----------------------------------------------------------------------
1284 # Scans a time in free format
1287 # string - String containing the time to scan
1288 # base - Base time, expressed in seconds from the Epoch
1289 # timezone - Default time zone in which the time will be expressed
1290 # locale - (Unused) Name of the locale where the time will be scanned.
1293 # Returns the date and time extracted from the string in seconds from
1296 #----------------------------------------------------------------------
1298 proc ::tcl::clock::FreeScan { string base timezone locale } {
1302 # Get the data for time changes in the given zone
1305 SetupTimeZone $timezone
1306 } on error {retval opts} {
1307 dict unset opts -errorinfo
1308 return -options $opts $retval
1311 # Extract year, month and day from the base time for the parser to use as
1314 set date [GetDateFields $base $TZData($timezone) 2361222]
1315 dict set date secondOfDay [expr {
1316 [dict get $date localSeconds] % 86400
1319 # Parse the date. The parser will return a list comprising date, time,
1320 # time zone, relative month/day/seconds, relative weekday, ordinal month.
1323 set scanned [Oldscan $string \
1324 [dict get $date year] \
1325 [dict get $date month] \
1326 [dict get $date dayOfMonth]]
1328 parseDate parseTime parseZone parseRel \
1329 parseWeekday parseOrdinalMonth
1330 } on error message {
1331 return -code error \
1332 "unable to convert date-time string \"$string\": $message"
1335 # If the caller supplied a date in the string, update the 'date' dict with
1336 # the value. If the caller didn't specify a time with the date, default to
1339 if { [llength $parseDate] > 0 } {
1340 lassign $parseDate y m d
1348 dict set date era CE
1349 dict set date year $y
1350 dict set date month $m
1351 dict set date dayOfMonth $d
1352 if { $parseTime eq {} } {
1357 # If the caller supplied a time zone in the string, it comes back as a
1358 # two-element list; the first element is the number of minutes east of
1359 # Greenwich, and the second is a Daylight Saving Time indicator (1 == yes,
1360 # 0 == no, -1 == unknown). We make it into a time zone indicator of
1363 if { [llength $parseZone] > 0 } {
1364 lassign $parseZone minEast dstFlag
1365 set timezone [FormatNumericTimeZone \
1366 [expr { 60 * $minEast + 3600 * $dstFlag }]]
1367 SetupTimeZone $timezone
1369 dict set date tzName $timezone
1371 # Assemble date, time, zone into seconds-from-epoch
1373 set date [GetJulianDayFromEraYearMonthDay $date[set date {}] 2361222]
1374 if { $parseTime ne {} } {
1375 dict set date secondOfDay $parseTime
1376 } elseif { [llength $parseWeekday] != 0
1377 || [llength $parseOrdinalMonth] != 0
1378 || ( [llength $parseRel] != 0
1379 && ( [lindex $parseRel 0] != 0
1380 || [lindex $parseRel 1] != 0 ) ) } {
1381 dict set date secondOfDay 0
1384 dict set date localSeconds [expr {
1386 + ( 86400 * wide([dict get $date julianDay]) )
1387 + [dict get $date secondOfDay]
1389 dict set date tzName $timezone
1390 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) 2361222]
1391 set seconds [dict get $date seconds]
1395 if { [llength $parseRel] > 0 } {
1396 lassign $parseRel relMonth relDay relSecond
1397 set seconds [add $seconds \
1398 $relMonth months $relDay days $relSecond seconds \
1399 -timezone $timezone -locale $locale]
1402 # Do relative weekday
1404 if { [llength $parseWeekday] > 0 } {
1405 lassign $parseWeekday dayOrdinal dayOfWeek
1406 set date2 [GetDateFields $seconds $TZData($timezone) 2361222]
1407 dict set date2 era CE
1408 set jdwkday [WeekdayOnOrBefore $dayOfWeek [expr {
1409 [dict get $date2 julianDay] + 6
1411 incr jdwkday [expr { 7 * $dayOrdinal }]
1412 if { $dayOrdinal > 0 } {
1415 dict set date2 secondOfDay \
1416 [expr { [dict get $date2 localSeconds] % 86400 }]
1417 dict set date2 julianDay $jdwkday
1418 dict set date2 localSeconds [expr {
1420 + ( 86400 * wide([dict get $date2 julianDay]) )
1421 + [dict get $date secondOfDay]
1423 dict set date2 tzName $timezone
1424 set date2 [ConvertLocalToUTC $date2[set date2 {}] $TZData($timezone) \
1426 set seconds [dict get $date2 seconds]
1432 if { [llength $parseOrdinalMonth] > 0 } {
1433 lassign $parseOrdinalMonth monthOrdinal monthNumber
1434 if { $monthOrdinal > 0 } {
1435 set monthDiff [expr { $monthNumber - [dict get $date month] }]
1436 if { $monthDiff <= 0 } {
1439 incr monthOrdinal -1
1441 set monthDiff [expr { [dict get $date month] - $monthNumber }]
1442 if { $monthDiff >= 0 } {
1447 set seconds [add $seconds $monthOrdinal years $monthDiff months \
1448 -timezone $timezone -locale $locale]
1455 #----------------------------------------------------------------------
1457 # ParseClockScanFormat --
1459 # Parses a format string given to [clock scan -format]
1462 # formatString - The format being parsed
1463 # locale - The current locale
1466 # Constructs and returns a procedure that accepts the string being
1467 # scanned, the base time, and the time zone. The procedure will either
1468 # return the scanned time or else throw an error that should be rethrown
1469 # to the caller of [clock scan]
1472 # The given procedure is defined in the ::tcl::clock namespace. Scan
1473 # procedures are not deleted once installed.
1475 # Why do we parse dates by defining a procedure to parse them? The reason is
1476 # that by doing so, we have one convenient place to cache all the information:
1477 # the regular expressions that match the patterns (which will be compiled),
1478 # the code that assembles the date information, everything lands in one place.
1479 # In this way, when a given format is reused at run time, all the information
1480 # of how to apply it is available in a single place.
1482 #----------------------------------------------------------------------
1484 proc ::tcl::clock::ParseClockScanFormat {formatString locale} {
1485 # Check whether the format has been parsed previously, and return the
1486 # existing recognizer if it has.
1488 set procName scanproc'$formatString'$locale
1489 set procName [namespace current]::[string map {: {\:} \\ {\\}} $procName]
1490 if { [namespace which $procName] != {} } {
1494 variable DateParseActions
1495 variable TimeParseActions
1497 # Localize the %x, %X, etc. groups
1499 set formatString [LocalizeFormat $locale $formatString]
1501 # Condense whitespace
1503 regsub -all {[[:space:]]+} $formatString { } formatString
1505 # Walk through the groups of the format string. In this loop, we
1507 # - a regular expression that matches the string,
1508 # - the count of capturing brackets in the regexp
1509 # - a set of code that post-processes the fields captured by the regexp,
1510 # - a dictionary whose keys are the names of fields that are present
1511 # in the format string.
1513 set re {^[[:space:]]*}
1516 set fieldSet [dict create]
1521 foreach c [split $formatString {}] {
1522 switch -exact -- $state {
1526 } elseif { $c eq " " } {
1527 append re {[[:space:]]+}
1529 if { ! [string is alnum $c] } {
1537 switch -exact -- $c {
1542 append re "\[\[:space:\]\]*"
1544 a - A { # Day of week, in words
1548 abr [mc DAYS_OF_WEEK_ABBREV] \
1549 full [mc DAYS_OF_WEEK_FULL] {
1550 dict set l [string tolower $abr] $i
1551 dict set l [string tolower $full] $i
1554 lassign [UniquePrefixRegexp $l] regex lookup
1555 append re ( $regex )
1556 dict set fieldSet dayOfWeek [incr fieldCount]
1557 append postcode "dict set date dayOfWeek \[" \
1558 "dict get " [list $lookup] " " \
1559 \[ {string tolower $field} [incr captureCount] \] \
1562 b - B - h { # Name of month
1566 abr [mc MONTHS_ABBREV] \
1567 full [mc MONTHS_FULL] {
1569 dict set l [string tolower $abr] $i
1570 dict set l [string tolower $full] $i
1572 lassign [UniquePrefixRegexp $l] regex lookup
1573 append re ( $regex )
1574 dict set fieldSet month [incr fieldCount]
1575 append postcode "dict set date month \[" \
1576 "dict get " [list $lookup] \
1577 " " \[ {string tolower $field} \
1578 [incr captureCount] \] \
1581 C { # Gregorian century
1582 append re \\s*(\\d\\d?)
1583 dict set fieldSet century [incr fieldCount]
1584 append postcode "dict set date century \[" \
1585 "::scan \$field" [incr captureCount] " %d" \
1588 d - e { # Day of month
1589 append re \\s*(\\d\\d?)
1590 dict set fieldSet dayOfMonth [incr fieldCount]
1591 append postcode "dict set date dayOfMonth \[" \
1592 "::scan \$field" [incr captureCount] " %d" \
1595 E { # Prefix for locale-specific codes
1598 g { # ISO8601 2-digit year
1599 append re \\s*(\\d\\d)
1600 dict set fieldSet iso8601YearOfCentury \
1603 "dict set date iso8601YearOfCentury \[" \
1604 "::scan \$field" [incr captureCount] " %d" \
1607 G { # ISO8601 4-digit year
1608 append re \\s*(\\d\\d)(\\d\\d)
1609 dict set fieldSet iso8601Century [incr fieldCount]
1610 dict set fieldSet iso8601YearOfCentury \
1613 "dict set date iso8601Century \[" \
1614 "::scan \$field" [incr captureCount] " %d" \
1616 "dict set date iso8601YearOfCentury \[" \
1617 "::scan \$field" [incr captureCount] " %d" \
1620 H - k { # Hour of day
1621 append re \\s*(\\d\\d?)
1622 dict set fieldSet hour [incr fieldCount]
1623 append postcode "dict set date hour \[" \
1624 "::scan \$field" [incr captureCount] " %d" \
1627 I - l { # Hour, AM/PM
1628 append re \\s*(\\d\\d?)
1629 dict set fieldSet hourAMPM [incr fieldCount]
1630 append postcode "dict set date hourAMPM \[" \
1631 "::scan \$field" [incr captureCount] " %d" \
1635 append re \\s*(\\d\\d?\\d?)
1636 dict set fieldSet dayOfYear [incr fieldCount]
1637 append postcode "dict set date dayOfYear \[" \
1638 "::scan \$field" [incr captureCount] " %d" \
1641 J { # Julian Day Number
1642 append re \\s*(\\d+)
1643 dict set fieldSet julianDay [incr fieldCount]
1644 append postcode "dict set date julianDay \[" \
1645 "::scan \$field" [incr captureCount] " %ld" \
1648 m - N { # Month number
1649 append re \\s*(\\d\\d?)
1650 dict set fieldSet month [incr fieldCount]
1651 append postcode "dict set date month \[" \
1652 "::scan \$field" [incr captureCount] " %d" \
1656 append re \\s*(\\d\\d?)
1657 dict set fieldSet minute [incr fieldCount]
1658 append postcode "dict set date minute \[" \
1659 "::scan \$field" [incr captureCount] " %d" \
1662 n { # Literal newline
1665 O { # Prefix for locale numerics
1668 p - P { # AM/PM indicator
1669 set l [list [string tolower [mc AM]] 0 \
1670 [string tolower [mc PM]] 1]
1671 lassign [UniquePrefixRegexp $l] regex lookup
1672 append re ( $regex )
1673 dict set fieldSet amPmIndicator [incr fieldCount]
1674 append postcode "dict set date amPmIndicator \[" \
1675 "dict get " [list $lookup] " \[string tolower " \
1677 [incr captureCount] \
1681 append re {Stardate\s+([-+]?\d+)(\d\d\d)[.](\d)}
1683 dict set fieldSet seconds [incr fieldCount]
1684 append postcode {dict set date seconds } \[ \
1685 {ParseStarDate $field} [incr captureCount] \
1686 { $field} [incr captureCount] \
1687 { $field} [incr captureCount] \
1690 s { # Seconds from Posix Epoch
1691 # This next case is insanely difficult, because it's
1692 # problematic to determine whether the field is
1693 # actually within the range of a wide integer.
1694 append re {\s*([-+]?\d+)}
1695 dict set fieldSet seconds [incr fieldCount]
1696 append postcode {dict set date seconds } \[ \
1697 {ScanWide $field} [incr captureCount] \] \n
1700 append re \\s*(\\d\\d?)
1701 dict set fieldSet second [incr fieldCount]
1702 append postcode "dict set date second \[" \
1703 "::scan \$field" [incr captureCount] " %d" \
1706 t { # Literal tab character
1709 u - w { # Day number within week, 0 or 7 == Sun
1712 dict set fieldSet dayOfWeek [incr fieldCount]
1713 append postcode {::scan $field} [incr captureCount] \
1718 } elseif { $dow > 7 } {
1719 return -code error \
1720 -errorcode [list CLOCK badDayOfWeek] \
1721 "day of week is greater than 7"
1723 dict set date dayOfWeek $dow
1726 U { # Week of year. The first Sunday of
1727 # the year is the first day of week
1728 # 01. No scan rule uses this group.
1729 append re \\s*\\d\\d?
1731 V { # Week of ISO8601 year
1733 append re \\s*(\\d\\d?)
1734 dict set fieldSet iso8601Week [incr fieldCount]
1735 append postcode "dict set date iso8601Week \[" \
1736 "::scan \$field" [incr captureCount] " %d" \
1739 W { # Week of the year (00-53). The first
1740 # Monday of the year is the first day
1741 # of week 01. No scan rule uses this
1743 append re \\s*\\d\\d?
1745 y { # Two-digit Gregorian year
1746 append re \\s*(\\d\\d?)
1747 dict set fieldSet yearOfCentury [incr fieldCount]
1748 append postcode "dict set date yearOfCentury \[" \
1749 "::scan \$field" [incr captureCount] " %d" \
1752 Y { # 4-digit Gregorian year
1753 append re \\s*(\\d\\d)(\\d\\d)
1754 dict set fieldSet century [incr fieldCount]
1755 dict set fieldSet yearOfCentury [incr fieldCount]
1757 "dict set date century \[" \
1758 "::scan \$field" [incr captureCount] " %d" \
1760 "dict set date yearOfCentury \[" \
1761 "::scan \$field" [incr captureCount] " %d" \
1764 z - Z { # Time zone name
1765 append re {(?:([-+]\d\d(?::?\d\d(?::?\d\d)?)?)|([[:alnum:]]{1,4}))}
1766 dict set fieldSet tzName [incr fieldCount]
1768 {if } \{ { $field} [incr captureCount] \
1769 { ne "" } \} { } \{ \n \
1770 {dict set date tzName $field} \
1773 {dict set date tzName } \[ \
1774 {ConvertLegacyTimeZone $field} \
1775 [incr captureCount] \] \n \
1778 % { # Literal percent character
1783 if { ! [string is alnum $c] } {
1791 switch -exact -- $c {
1792 C { # Locale-dependent era
1794 foreach triple [mc LOCALE_ERAS] {
1795 lassign $triple t symbol year
1796 dict set d [string tolower $symbol] $year
1798 lassign [UniquePrefixRegexp $d] regex lookup
1799 append re (?: $regex )
1803 dict set l [string tolower [mc BCE]] BCE
1804 dict set l [string tolower [mc CE]] CE
1805 dict set l b.c.e. BCE
1809 lassign [UniquePrefixRegexp $l] regex lookup
1810 append re ( $regex )
1811 dict set fieldSet era [incr fieldCount]
1812 append postcode "dict set date era \["\
1813 "dict get " [list $lookup] \
1814 { } \[ {string tolower $field} \
1815 [incr captureCount] \] \
1818 y { # Locale-dependent year of the era
1819 lassign [LocaleNumeralMatcher $locale] regex lookup
1825 if { ! [string is alnum $c] } {
1834 switch -exact -- $c {
1836 lassign [LocaleNumeralMatcher $locale] regex lookup
1838 dict set fieldSet dayOfMonth [incr fieldCount]
1839 append postcode "dict set date dayOfMonth \[" \
1840 "dict get " [list $lookup] " \$field" \
1841 [incr captureCount] \
1845 lassign [LocaleNumeralMatcher $locale] regex lookup
1847 dict set fieldSet hour [incr fieldCount]
1848 append postcode "dict set date hour \[" \
1849 "dict get " [list $lookup] " \$field" \
1850 [incr captureCount] \
1854 lassign [LocaleNumeralMatcher $locale] regex lookup
1856 dict set fieldSet hourAMPM [incr fieldCount]
1857 append postcode "dict set date hourAMPM \[" \
1858 "dict get " [list $lookup] " \$field" \
1859 [incr captureCount] \
1863 lassign [LocaleNumeralMatcher $locale] regex lookup
1865 dict set fieldSet month [incr fieldCount]
1866 append postcode "dict set date month \[" \
1867 "dict get " [list $lookup] " \$field" \
1868 [incr captureCount] \
1872 lassign [LocaleNumeralMatcher $locale] regex lookup
1874 dict set fieldSet minute [incr fieldCount]
1875 append postcode "dict set date minute \[" \
1876 "dict get " [list $lookup] " \$field" \
1877 [incr captureCount] \
1881 lassign [LocaleNumeralMatcher $locale] regex lookup
1883 dict set fieldSet second [incr fieldCount]
1884 append postcode "dict set date second \[" \
1885 "dict get " [list $lookup] " \$field" \
1886 [incr captureCount] \
1890 lassign [LocaleNumeralMatcher $locale] regex lookup
1892 dict set fieldSet dayOfWeek [incr fieldCount]
1893 append postcode "set dow \[dict get " [list $lookup] \
1894 { $field} [incr captureCount] \] \n \
1898 } elseif { $dow > 7 } {
1899 return -code error \
1900 -errorcode [list CLOCK badDayOfWeek] \
1901 "day of week is greater than 7"
1903 dict set date dayOfWeek $dow
1907 lassign [LocaleNumeralMatcher $locale] regex lookup
1909 dict set fieldSet yearOfCentury [incr fieldCount]
1910 append postcode {dict set date yearOfCentury } \[ \
1911 {dict get } [list $lookup] { $field} \
1912 [incr captureCount] \] \n
1916 if { ! [string is alnum $c] } {
1927 # Clean up any unfinished format groups
1929 append re $state \\s*\$
1931 # Build the procedure
1934 append procBody "variable ::tcl::clock::TZData" \n
1935 append procBody "if \{ !\[ regexp -nocase [list $re] \$string ->"
1936 for { set i 1 } { $i <= $captureCount } { incr i } {
1937 append procBody " " field $i
1939 append procBody "\] \} \{" \n
1941 return -code error -errorcode [list CLOCK badInputString] \
1942 {input string does not match supplied format}
1944 append procBody \}\n
1945 append procBody "set date \[dict create\]" \n
1946 append procBody {dict set date tzName $timeZone} \n
1947 append procBody $postcode
1948 append procBody [list set changeover [mc GREGORIAN_CHANGE_DATE]] \n
1950 # Set up the time zone before doing anything with a default base date
1951 # that might need a timezone to interpret it.
1953 if { ![dict exists $fieldSet seconds]
1954 && ![dict exists $fieldSet starDate] } {
1955 if { [dict exists $fieldSet tzName] } {
1957 set timeZone [dict get $date tzName]
1961 ::tcl::clock::SetupTimeZone $timeZone
1965 # Add code that gets Julian Day Number from the fields.
1967 append procBody [MakeParseCodeFromFields $fieldSet $DateParseActions]
1971 append procBody [MakeParseCodeFromFields $fieldSet $TimeParseActions]
1973 # Assemble seconds from the Julian day and second of the day.
1974 # Convert to local time unless epoch seconds or stardate are
1975 # being processed - they're always absolute
1977 if { ![dict exists $fieldSet seconds]
1978 && ![dict exists $fieldSet starDate] } {
1980 if { [dict get $date julianDay] > 5373484 } {
1981 return -code error -errorcode [list CLOCK dateTooLarge] \
1982 "requested date too large to represent"
1984 dict set date localSeconds [expr {
1986 + ( 86400 * wide([dict get $date julianDay]) )
1987 + [dict get $date secondOfDay]
1991 # Finally, convert the date to local time
1994 set date [::tcl::clock::ConvertLocalToUTC $date[set date {}] \
1995 $TZData($timeZone) $changeover]
2001 append procBody {return [dict get $date seconds]} \n
2003 proc $procName { string baseTime timeZone } $procBody
2005 # puts [list proc $procName [list string baseTime timeZone] $procBody]
2010 #----------------------------------------------------------------------
2012 # LocaleNumeralMatcher --
2014 # Composes a regexp that captures the numerals in the given locale, and
2015 # a dictionary to map them to conventional numerals.
2018 # locale - Name of the current locale
2021 # Returns a two-element list comprising the regexp and the dictionary.
2024 # Caches the result.
2026 #----------------------------------------------------------------------
2028 proc ::tcl::clock::LocaleNumeralMatcher {l} {
2029 variable LocaleNumeralCache
2031 if { ![dict exists $LocaleNumeralCache $l] } {
2035 foreach n [mc LOCALE_NUMERALS] {
2037 regsub -all {[^[:alnum:]]} $n \\\\& subex
2038 append re $sep $subex
2043 dict set LocaleNumeralCache $l [list $re $d]
2045 return [dict get $LocaleNumeralCache $l]
2050 #----------------------------------------------------------------------
2052 # UniquePrefixRegexp --
2054 # Composes a regexp that performs unique-prefix matching. The RE
2055 # matches one of a supplied set of strings, or any unique prefix
2059 # data - List of alternating match-strings and values.
2060 # Match-strings with distinct values are considered
2064 # Returns a two-element list. The first is a regexp that matches any
2065 # unique prefix of any of the strings. The second is a dictionary whose
2066 # keys are match values from the regexp and whose values are the
2067 # corresponding values from 'data'.
2072 #----------------------------------------------------------------------
2074 proc ::tcl::clock::UniquePrefixRegexp { data } {
2075 # The 'successors' dictionary will contain, for each string that is a
2076 # prefix of any key, all characters that may follow that prefix. The
2077 # 'prefixMapping' dictionary will have keys that are prefixes of keys and
2078 # values that correspond to the keys.
2080 set prefixMapping [dict create]
2081 set successors [dict create {} {}]
2083 # Walk the key-value pairs
2085 foreach { key value } $data {
2086 # Construct all prefixes of the key;
2089 foreach char [split $key {}] {
2090 set oldPrefix $prefix
2091 dict set successors $oldPrefix $char {}
2094 # Put the prefixes in the 'prefixMapping' and 'successors'
2097 dict lappend prefixMapping $prefix $value
2098 if { ![dict exists $successors $prefix] } {
2099 dict set successors $prefix {}
2104 # Identify those prefixes that designate unique values, and those that are
2107 set uniquePrefixMapping {}
2108 dict for { key valueList } $prefixMapping {
2109 if { [llength $valueList] == 1 } {
2110 dict set uniquePrefixMapping $key [lindex $valueList 0]
2113 foreach { key value } $data {
2114 dict set uniquePrefixMapping $key $value
2120 [MakeUniquePrefixRegexp $successors $uniquePrefixMapping {}] \
2121 $uniquePrefixMapping]
2124 #----------------------------------------------------------------------
2126 # MakeUniquePrefixRegexp --
2128 # Service procedure for 'UniquePrefixRegexp' that constructs a regular
2129 # expresison that matches the unique prefixes.
2132 # successors - Dictionary whose keys are all prefixes
2133 # of keys passed to 'UniquePrefixRegexp' and whose
2134 # values are dictionaries whose keys are the characters
2135 # that may follow those prefixes.
2136 # uniquePrefixMapping - Dictionary whose keys are the unique
2137 # prefixes and whose values are not examined.
2138 # prefixString - Current prefix being processed.
2141 # Returns a constructed regular expression that matches the set of
2142 # unique prefixes beginning with the 'prefixString'.
2147 #----------------------------------------------------------------------
2149 proc ::tcl::clock::MakeUniquePrefixRegexp { successors
2153 # Get the characters that may follow the current prefix string
2155 set schars [lsort -ascii [dict keys [dict get $successors $prefixString]]]
2156 if { [llength $schars] == 0 } {
2160 # If there is more than one successor character, or if the current prefix
2161 # is a unique prefix, surround the generated re with non-capturing
2166 [dict exists $uniquePrefixMapping $prefixString]
2167 || [llength $schars] > 1
2172 # Generate a regexp that matches the successors.
2175 foreach { c } $schars {
2176 set nextPrefix $prefixString$c
2177 regsub -all {[^[:alnum:]]} $c \\\\& rechar
2178 append re $sep $rechar \
2179 [MakeUniquePrefixRegexp \
2180 $successors $uniquePrefixMapping $nextPrefix]
2184 # If the current prefix is a unique prefix, make all following text
2185 # optional. Otherwise, if there is more than one successor character,
2186 # close the non-capturing parentheses.
2188 if { [dict exists $uniquePrefixMapping $prefixString] } {
2190 } elseif { [llength $schars] > 1 } {
2197 #----------------------------------------------------------------------
2199 # MakeParseCodeFromFields --
2201 # Composes Tcl code to extract the Julian Day Number from a dictionary
2202 # containing date fields.
2205 # dateFields -- Dictionary whose keys are fields of the date,
2206 # and whose values are the rightmost positions
2207 # at which those fields appear.
2208 # parseActions -- List of triples: field set, priority, and
2209 # code to emit. Smaller priorities are better, and
2210 # the list must be in ascending order by priority
2213 # Returns a burst of code that extracts the day number from the given
2219 #----------------------------------------------------------------------
2221 proc ::tcl::clock::MakeParseCodeFromFields { dateFields parseActions } {
2224 set currFieldPos [list]
2226 error "in ::tcl::clock::MakeParseCodeFromFields: can't happen"
2229 foreach { fieldSet prio parseAction } $parseActions {
2230 # If we've found an answer that's better than any that follow, quit
2233 if { $prio > $currPrio } {
2237 # Accumulate the field positions that are used in the current field
2242 foreach field $fieldSet {
2243 if { ! [dict exists $dateFields $field] } {
2247 lappend fieldPos [dict get $dateFields $field]
2250 # Quit if we don't have a complete set of fields
2255 # Determine whether the current answer is better than the last.
2257 set fPos [lsort -integer -decreasing $fieldPos]
2259 if { $prio == $currPrio } {
2260 foreach currPos $currFieldPos newPos $fPos {
2262 ![string is integer $newPos]
2263 || ![string is integer $currPos]
2264 || $newPos > $currPos
2268 if { $newPos < $currPos } {
2278 # Remember the best possibility for extracting date information
2281 set currFieldPos $fPos
2282 set currCodeBurst $parseAction
2285 return $currCodeBurst
2288 #----------------------------------------------------------------------
2292 # Switch [mclocale] to a given locale if necessary
2295 # locale -- Desired locale
2298 # Returns the locale that was previously current.
2301 # Does [mclocale]. If necessary, loades the designated locale's files.
2303 #----------------------------------------------------------------------
2305 proc ::tcl::clock::EnterLocale { locale } {
2306 if { $locale eq {system} } {
2307 if { $::tcl_platform(platform) ne {windows} } {
2308 # On a non-windows platform, the 'system' locale is the same as
2309 # the 'current' locale
2313 # On a windows platform, the 'system' locale is adapted from the
2314 # 'current' locale by applying the date and time formats from the
2315 # Control Panel. First, load the 'current' locale if it's not yet
2318 mcpackagelocale set [mclocale]
2320 # Make a new locale string for the system locale, and get the
2321 # Control Panel information
2323 set locale [mclocale]_windows
2324 if { ! [mcpackagelocale present $locale] } {
2325 LoadWindowsDateTimeFormats $locale
2329 if { $locale eq {current}} {
2330 set locale [mclocale]
2332 # Eventually load the locale
2333 mcpackagelocale set $locale
2336 #----------------------------------------------------------------------
2338 # LoadWindowsDateTimeFormats --
2340 # Load the date/time formats from the Control Panel in Windows and
2341 # convert them so that they're usable by Tcl.
2344 # locale - Name of the locale in whose message catalog
2345 # the converted formats are to be stored.
2351 # Updates the given message catalog with the locale strings.
2353 # Presumes that on entry, [mclocale] is set to the current locale, so that
2354 # default strings can be obtained if the Registry query fails.
2356 #----------------------------------------------------------------------
2358 proc ::tcl::clock::LoadWindowsDateTimeFormats { locale } {
2359 # Bail out if we can't find the Registry
2362 if { [info exists NoRegistry] } return
2365 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2370 foreach { unquoted quoted } [split $string '] {
2371 append datefmt $quote [string map {
2385 if { $quoted eq {} } {
2391 ::msgcat::mcset $locale DATE_FORMAT $datefmt
2395 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2400 foreach { unquoted quoted } [split $string '] {
2401 append ldatefmt $quote [string map {
2415 if { $quoted eq {} } {
2421 ::msgcat::mcset $locale LOCALE_DATE_FORMAT $ldatefmt
2425 registry get "HKEY_CURRENT_USER\\Control Panel\\International" \
2430 foreach { unquoted quoted } [split $string '] {
2431 append timefmt $quote [string map {
2443 if { $quoted eq {} } {
2449 ::msgcat::mcset $locale TIME_FORMAT $timefmt
2453 ::msgcat::mcset $locale DATE_TIME_FORMAT "$datefmt $timefmt"
2456 ::msgcat::mcset $locale LOCALE_DATE_TIME_FORMAT "$ldatefmt $timefmt"
2463 #----------------------------------------------------------------------
2467 # Map away locale-dependent format groups in a clock format.
2470 # locale -- Current [mclocale] locale, supplied to avoid
2472 # format -- Format supplied to [clock scan] or [clock format]
2475 # Returns the string with locale-dependent composite format groups
2481 #----------------------------------------------------------------------
2483 proc ::tcl::clock::LocalizeFormat { locale format } {
2485 # message catalog key to cache this format
2486 set key FORMAT_$format
2488 if { [::msgcat::mcexists -exactlocale -exactnamespace $key] } {
2491 # Handle locale-dependent format groups by mapping them out of the format
2492 # string. Note that the order of the [string map] operations is
2493 # significant because later formats can refer to later ones; for example
2494 # %c can refer to %X, which in turn can refer to %T.
2499 %+ {%a %b %e %H:%M:%S %Z %Y}
2501 lappend list %EY [string map $list [mc LOCALE_YEAR_FORMAT]]
2502 lappend list %T [string map $list [mc TIME_FORMAT_24_SECS]]
2503 lappend list %R [string map $list [mc TIME_FORMAT_24]]
2504 lappend list %r [string map $list [mc TIME_FORMAT_12]]
2505 lappend list %X [string map $list [mc TIME_FORMAT]]
2506 lappend list %EX [string map $list [mc LOCALE_TIME_FORMAT]]
2507 lappend list %x [string map $list [mc DATE_FORMAT]]
2508 lappend list %Ex [string map $list [mc LOCALE_DATE_FORMAT]]
2509 lappend list %c [string map $list [mc DATE_TIME_FORMAT]]
2510 lappend list %Ec [string map $list [mc LOCALE_DATE_TIME_FORMAT]]
2511 set format [string map $list $format]
2513 ::msgcat::mcset $locale $key $format
2517 #----------------------------------------------------------------------
2519 # FormatNumericTimeZone --
2521 # Formats a time zone as +hhmmss
2524 # z - Time zone in seconds east of Greenwich
2527 # Returns the time zone formatted in a numeric form
2532 #----------------------------------------------------------------------
2534 proc ::tcl::clock::FormatNumericTimeZone { z } {
2536 set z [expr { - $z }]
2541 append retval [::format %02d [expr { $z / 3600 }]]
2542 set z [expr { $z % 3600 }]
2543 append retval [::format %02d [expr { $z / 60 }]]
2544 set z [expr { $z % 60 }]
2546 append retval [::format %02d $z]
2551 #----------------------------------------------------------------------
2555 # Formats a date as a StarDate.
2558 # date - Dictionary containing 'year', 'dayOfYear', and
2559 # 'localSeconds' fields.
2562 # Returns the given date formatted as a StarDate.
2567 # Jeff Hobbs put this in to support an atrocious pun about Tcl being
2568 # "Enterprise ready." Now we're stuck with it.
2570 #----------------------------------------------------------------------
2572 proc ::tcl::clock::FormatStarDate { date } {
2573 variable Roddenberry
2575 # Get day of year, zero based
2577 set doy [expr { [dict get $date dayOfYear] - 1 }]
2579 # Determine whether the year is a leap year
2581 set lp [IsGregorianLeapYear $date]
2583 # Convert day of year to a fractional year
2586 set fractYear [expr { 1000 * $doy / 366 }]
2588 set fractYear [expr { 1000 * $doy / 365 }]
2591 # Put together the StarDate
2593 return [::format "Stardate %02d%03d.%1d" \
2594 [expr { [dict get $date year] - $Roddenberry }] \
2596 [expr { [dict get $date localSeconds] % 86400
2597 / ( 86400 / 10 ) }]]
2600 #----------------------------------------------------------------------
2607 # year - Year from the Roddenberry epoch
2608 # fractYear - Fraction of a year specifiying the day of year.
2609 # fractDay - Fraction of a day
2612 # Returns a count of seconds from the Posix epoch.
2617 # Jeff Hobbs put this in to support an atrocious pun about Tcl being
2618 # "Enterprise ready." Now we're stuck with it.
2620 #----------------------------------------------------------------------
2622 proc ::tcl::clock::ParseStarDate { year fractYear fractDay } {
2623 variable Roddenberry
2625 # Build a tentative date from year and fraction.
2627 set date [dict create \
2630 year [expr { $year + $Roddenberry }] \
2631 dayOfYear [expr { $fractYear * 365 / 1000 + 1 }]]
2632 set date [GetJulianDayFromGregorianEraYearDay $date[set date {}]]
2634 # Determine whether the given year is a leap year
2636 set lp [IsGregorianLeapYear $date]
2638 # Reconvert the fractional year according to whether the given year is a
2642 dict set date dayOfYear \
2643 [expr { $fractYear * 366 / 1000 + 1 }]
2645 dict set date dayOfYear \
2646 [expr { $fractYear * 365 / 1000 + 1 }]
2648 dict unset date julianDay
2649 dict unset date gregorian
2650 set date [GetJulianDayFromGregorianEraYearDay $date[set date {}]]
2653 86400 * [dict get $date julianDay]
2655 + ( 86400 / 10 ) * $fractDay
2659 #----------------------------------------------------------------------
2663 # Scans a wide integer from an input
2666 # str - String containing a decimal wide integer
2669 # Returns the string as a pure wide integer. Throws an error if the
2670 # string is misformatted or out of range.
2672 #----------------------------------------------------------------------
2674 proc ::tcl::clock::ScanWide { str } {
2675 set count [::scan $str {%ld %c} result junk]
2676 if { $count != 1 } {
2677 return -code error -errorcode [list CLOCK notAnInteger $str] \
2678 "\"$str\" is not an integer"
2680 if { [incr result 0] != $str } {
2681 return -code error -errorcode [list CLOCK integervalueTooLarge] \
2682 "integer value too large to represent"
2687 #----------------------------------------------------------------------
2689 # InterpretTwoDigitYear --
2691 # Given a date that contains only the year of the century, determines
2692 # the target value of a two-digit year.
2695 # date - Dictionary containing fields of the date.
2696 # baseTime - Base time relative to which the date is expressed.
2697 # twoDigitField - Name of the field that stores the two-digit year.
2698 # Default is 'yearOfCentury'
2699 # fourDigitField - Name of the field that will receive the four-digit
2700 # year. Default is 'year'
2703 # Returns the dictionary augmented with the four-digit year, stored in
2709 # The current rule for interpreting a two-digit year is that the year shall be
2710 # between 1937 and 2037, thus staying within the range of a 32-bit signed
2711 # value for time. This rule may change to a sliding window in future
2712 # versions, so the 'baseTime' parameter (which is currently ignored) is
2713 # provided in the procedure signature.
2715 #----------------------------------------------------------------------
2717 proc ::tcl::clock::InterpretTwoDigitYear { date baseTime
2718 { twoDigitField yearOfCentury }
2719 { fourDigitField year } } {
2720 set yr [dict get $date $twoDigitField]
2722 dict set date $fourDigitField [expr { $yr + 2000 }]
2724 dict set date $fourDigitField [expr { $yr + 1900 }]
2729 #----------------------------------------------------------------------
2733 # Places the number of the current year into a dictionary.
2736 # date - Dictionary value to update
2737 # baseTime - Base time from which to extract the year, expressed
2738 # in seconds from the Posix epoch
2739 # timezone - the time zone in which the date is being scanned
2740 # changeover - the Julian Day on which the Gregorian calendar
2741 # was adopted in the target locale.
2744 # Returns the dictionary with the current year assigned.
2749 #----------------------------------------------------------------------
2751 proc ::tcl::clock::AssignBaseYear { date baseTime timezone changeover } {
2754 # Find the Julian Day Number corresponding to the base time, and
2755 # find the Gregorian year corresponding to that Julian Day.
2757 set date2 [GetDateFields $baseTime $TZData($timezone) $changeover]
2759 # Store the converted year
2761 dict set date era [dict get $date2 era]
2762 dict set date year [dict get $date2 year]
2767 #----------------------------------------------------------------------
2769 # AssignBaseIso8601Year --
2771 # Determines the base year in the ISO8601 fiscal calendar.
2774 # date - Dictionary containing the fields of the date that
2775 # is to be augmented with the base year.
2776 # baseTime - Base time expressed in seconds from the Posix epoch.
2777 # timeZone - Target time zone
2778 # changeover - Julian Day of adoption of the Gregorian calendar in
2779 # the target locale.
2782 # Returns the given date with "iso8601Year" set to the
2788 #----------------------------------------------------------------------
2790 proc ::tcl::clock::AssignBaseIso8601Year {date baseTime timeZone changeover} {
2793 # Find the Julian Day Number corresponding to the base time
2795 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2797 # Calculate the ISO8601 date and transfer the year
2799 dict set date era CE
2800 dict set date iso8601Year [dict get $date2 iso8601Year]
2804 #----------------------------------------------------------------------
2806 # AssignBaseMonth --
2808 # Places the number of the current year and month into a
2812 # date - Dictionary value to update
2813 # baseTime - Time from which the year and month are to be
2814 # obtained, expressed in seconds from the Posix epoch.
2815 # timezone - Name of the desired time zone
2816 # changeover - Julian Day on which the Gregorian calendar was adopted.
2819 # Returns the dictionary with the base year and month assigned.
2824 #----------------------------------------------------------------------
2826 proc ::tcl::clock::AssignBaseMonth {date baseTime timezone changeover} {
2829 # Find the year and month corresponding to the base time
2831 set date2 [GetDateFields $baseTime $TZData($timezone) $changeover]
2832 dict set date era [dict get $date2 era]
2833 dict set date year [dict get $date2 year]
2834 dict set date month [dict get $date2 month]
2838 #----------------------------------------------------------------------
2842 # Determines the base year and week in the ISO8601 fiscal calendar.
2845 # date - Dictionary containing the fields of the date that
2846 # is to be augmented with the base year and week.
2847 # baseTime - Base time expressed in seconds from the Posix epoch.
2848 # changeover - Julian Day on which the Gregorian calendar was adopted
2849 # in the target locale.
2852 # Returns the given date with "iso8601Year" set to the
2853 # base year and "iso8601Week" to the week number.
2858 #----------------------------------------------------------------------
2860 proc ::tcl::clock::AssignBaseWeek {date baseTime timeZone changeover} {
2863 # Find the Julian Day Number corresponding to the base time
2865 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2867 # Calculate the ISO8601 date and transfer the year
2869 dict set date era CE
2870 dict set date iso8601Year [dict get $date2 iso8601Year]
2871 dict set date iso8601Week [dict get $date2 iso8601Week]
2875 #----------------------------------------------------------------------
2877 # AssignBaseJulianDay --
2879 # Determines the base day for a time-of-day conversion.
2882 # date - Dictionary that is to get the base day
2883 # baseTime - Base time expressed in seconds from the Posix epoch
2884 # changeover - Julian day on which the Gregorian calendar was
2885 # adpoted in the target locale.
2888 # Returns the given dictionary augmented with a 'julianDay' field
2889 # that contains the base day.
2894 #----------------------------------------------------------------------
2896 proc ::tcl::clock::AssignBaseJulianDay { date baseTime timeZone changeover } {
2899 # Find the Julian Day Number corresponding to the base time
2901 set date2 [GetDateFields $baseTime $TZData($timeZone) $changeover]
2902 dict set date julianDay [dict get $date2 julianDay]
2907 #----------------------------------------------------------------------
2911 # Interprets a time in the form "hh:mm:ss am".
2914 # date -- Dictionary containing "hourAMPM", "minute", "second"
2915 # and "amPmIndicator" fields.
2918 # Returns the number of seconds from local midnight.
2923 #----------------------------------------------------------------------
2925 proc ::tcl::clock::InterpretHMSP { date } {
2926 set hr [dict get $date hourAMPM]
2930 if { [dict get $date amPmIndicator] } {
2933 dict set date hour $hr
2934 return [InterpretHMS $date[set date {}]]
2937 #----------------------------------------------------------------------
2941 # Interprets a 24-hour time "hh:mm:ss"
2944 # date -- Dictionary containing the "hour", "minute" and "second"
2948 # Returns the given dictionary augmented with a "secondOfDay"
2949 # field containing the number of seconds from local midnight.
2954 #----------------------------------------------------------------------
2956 proc ::tcl::clock::InterpretHMS { date } {
2958 ( [dict get $date hour] * 60
2959 + [dict get $date minute] ) * 60
2960 + [dict get $date second]
2964 #----------------------------------------------------------------------
2966 # GetSystemTimeZone --
2968 # Determines the system time zone, which is the default for the
2969 # 'clock' command if no other zone is supplied.
2975 # Returns the system time zone.
2978 # Stores the sustem time zone in the 'CachedSystemTimeZone'
2979 # variable, since determining it may be an expensive process.
2981 #----------------------------------------------------------------------
2983 proc ::tcl::clock::GetSystemTimeZone {} {
2984 variable CachedSystemTimeZone
2985 variable TimeZoneBad
2987 if {[set result [getenv TCL_TZ]] ne {}} {
2988 set timezone $result
2989 } elseif {[set result [getenv TZ]] ne {}} {
2990 set timezone $result
2992 # Cache the time zone only if it was detected by one of the
2993 # expensive methods.
2994 if { [info exists CachedSystemTimeZone] } {
2995 set timezone $CachedSystemTimeZone
2996 } elseif { $::tcl_platform(platform) eq {windows} } {
2997 set timezone [GuessWindowsTimeZone]
2998 } elseif { [file exists /etc/localtime]
2999 && ![catch {ReadZoneinfoFile \
3000 Tcl/Localtime /etc/localtime}] } {
3001 set timezone :Tcl/Localtime
3003 set timezone :localtime
3005 set CachedSystemTimeZone $timezone
3007 if { ![dict exists $TimeZoneBad $timezone] } {
3008 dict set TimeZoneBad $timezone [catch {SetupTimeZone $timezone}]
3010 if { [dict get $TimeZoneBad $timezone] } {
3017 #----------------------------------------------------------------------
3019 # ConvertLegacyTimeZone --
3021 # Given an alphanumeric time zone identifier and the system time zone,
3022 # convert the alphanumeric identifier to an unambiguous time zone.
3025 # tzname - Name of the time zone to convert
3028 # Returns a time zone name corresponding to tzname, but in an
3029 # unambiguous form, generally +hhmm.
3031 # This procedure is implemented primarily to allow the parsing of RFC822
3032 # date/time strings. Processing a time zone name on input is not recommended
3033 # practice, because there is considerable room for ambiguity; for instance, is
3034 # BST Brazilian Standard Time, or British Summer Time?
3036 #----------------------------------------------------------------------
3038 proc ::tcl::clock::ConvertLegacyTimeZone { tzname } {
3039 variable LegacyTimeZone
3041 set tzname [string tolower $tzname]
3042 if { ![dict exists $LegacyTimeZone $tzname] } {
3043 return -code error -errorcode [list CLOCK badTZName $tzname] \
3044 "time zone \"$tzname\" not found"
3046 return [dict get $LegacyTimeZone $tzname]
3049 #----------------------------------------------------------------------
3053 # Given the name or specification of a time zone, sets up its in-memory
3057 # tzname - Name of a time zone
3060 # Unless the time zone is ':localtime', sets the TZData array to contain
3061 # the lookup table for local<->UTC conversion. Returns an error if the
3062 # time zone cannot be parsed.
3064 #----------------------------------------------------------------------
3066 proc ::tcl::clock::SetupTimeZone { timezone } {
3069 if {! [info exists TZData($timezone)] } {
3071 if { $timezone eq {:localtime} } {
3072 # Nothing to do, we'll convert using the localtime function
3075 [regexp {^([-+])(\d\d)(?::?(\d\d)(?::?(\d\d))?)?} $timezone \
3078 # Make a fixed offset
3091 set offset [expr { ( $hh * 60 + $mm ) * 60 + $ss }]
3093 set offset [expr { - $offset }]
3095 set TZData($timezone) [list [list $MINWIDE $offset -1 $timezone]]
3097 } elseif { [string index $timezone 0] eq {:} } {
3098 # Convert using a time zone file
3102 LoadTimeZoneFile [string range $timezone 1 end]
3104 LoadZoneinfoFile [string range $timezone 1 end]
3107 return -code error \
3108 -errorcode [list CLOCK badTimeZone $timezone] \
3109 "time zone \"$timezone\" not found"
3111 } elseif { ![catch {ParsePosixTimeZone $timezone} tzfields] } {
3112 # This looks like a POSIX time zone - try to process it
3114 if { [catch {ProcessPosixTimeZone $tzfields} data opts] } {
3115 if { [lindex [dict get $opts -errorcode] 0] eq {CLOCK} } {
3116 dict unset opts -errorinfo
3118 return -options $opts $data
3120 set TZData($timezone) $data
3124 # We couldn't parse this as a POSIX time zone. Try again with a
3125 # time zone file - this time without a colon
3127 if { [catch { LoadTimeZoneFile $timezone }]
3128 && [catch { LoadZoneinfoFile $timezone } - opts] } {
3129 dict unset opts -errorinfo
3130 return -options $opts "time zone $timezone not found"
3132 set TZData($timezone) $TZData(:$timezone)
3139 #----------------------------------------------------------------------
3141 # GuessWindowsTimeZone --
3143 # Determines the system time zone on windows.
3149 # Returns a time zone specifier that corresponds to the system time zone
3150 # information found in the Registry.
3153 # Fixed dates for DST change are unimplemented at present, because no
3154 # time zone information supplied with Windows actually uses them!
3156 # On a Windows system where neither $env(TCL_TZ) nor $env(TZ) is specified,
3157 # GuessWindowsTimeZone looks in the Registry for the system time zone
3158 # information. It then attempts to find an entry in WinZoneInfo for a time
3159 # zone that uses the same rules. If it finds one, it returns it; otherwise,
3160 # it constructs a Posix-style time zone string and returns that.
3162 #----------------------------------------------------------------------
3164 proc ::tcl::clock::GuessWindowsTimeZone {} {
3165 variable WinZoneInfo
3167 variable TimeZoneBad
3169 if { [info exists NoRegistry] } {
3173 # Dredge time zone information out of the registry
3176 set rpath HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\TimeZoneInformation
3179 * [registry get $rpath Bias] }] \
3181 * [registry get $rpath StandardBias] }] \
3183 * [registry get $rpath DaylightBias] }]]
3184 set stdtzi [registry get $rpath StandardStart]
3185 foreach ind {0 2 14 4 6 8 10 12} {
3186 binary scan $stdtzi @${ind}s val
3189 set daytzi [registry get $rpath DaylightStart]
3190 foreach ind {0 2 14 4 6 8 10 12} {
3191 binary scan $daytzi @${ind}s val
3195 # Missing values in the Registry - bail out
3200 # Make up a Posix time zone specifier if we can't find one. Check here
3201 # that the tzdata file exists, in case we're running in an environment
3202 # (e.g. starpack) where tzdata is incomplete. (Bug 1237907)
3204 if { [dict exists $WinZoneInfo $data] } {
3205 set tzname [dict get $WinZoneInfo $data]
3206 if { ! [dict exists $TimeZoneBad $tzname] } {
3207 dict set TimeZoneBad $tzname [catch {SetupTimeZone $tzname}]
3212 if { $tzname eq {} || [dict get $TimeZoneBad $tzname] } {
3214 bias stdBias dstBias \
3215 stdYear stdMonth stdDayOfWeek stdDayOfMonth \
3216 stdHour stdMinute stdSecond stdMillisec \
3217 dstYear dstMonth dstDayOfWeek dstDayOfMonth \
3218 dstHour dstMinute dstSecond dstMillisec
3219 set stdDelta [expr { $bias + $stdBias }]
3220 set dstDelta [expr { $bias + $dstBias }]
3221 if { $stdDelta <= 0 } {
3223 set stdDelta [expr { - $stdDelta }]
3229 set hh [::format %02d [expr { $stdDelta / 3600 }]]
3230 set mm [::format %02d [expr { ($stdDelta / 60 ) % 60 }]]
3231 set ss [::format %02d [expr { $stdDelta % 60 }]]
3233 append tzname < $dispStdSignum $hh $mm > $stdSignum $hh : $mm : $ss
3234 if { $stdMonth >= 0 } {
3235 if { $dstDelta <= 0 } {
3237 set dstDelta [expr { - $dstDelta }]
3243 set hh [::format %02d [expr { $dstDelta / 3600 }]]
3244 set mm [::format %02d [expr { ($dstDelta / 60 ) % 60 }]]
3245 set ss [::format %02d [expr { $dstDelta % 60 }]]
3246 append tzname < $dispDstSignum $hh $mm > $dstSignum $hh : $mm : $ss
3247 if { $dstYear == 0 } {
3248 append tzname ,M $dstMonth . $dstDayOfMonth . $dstDayOfWeek
3250 # I have not been able to find any locale on which Windows
3251 # converts time zone on a fixed day of the year, hence don't
3252 # know how to interpret the fields. If someone can inform me,
3253 # I'd be glad to code it up. For right now, we bail out in
3257 append tzname / [::format %02d $dstHour] \
3258 : [::format %02d $dstMinute] \
3259 : [::format %02d $dstSecond]
3260 if { $stdYear == 0 } {
3261 append tzname ,M $stdMonth . $stdDayOfMonth . $stdDayOfWeek
3263 # I have not been able to find any locale on which Windows
3264 # converts time zone on a fixed day of the year, hence don't
3265 # know how to interpret the fields. If someone can inform me,
3266 # I'd be glad to code it up. For right now, we bail out in
3270 append tzname / [::format %02d $stdHour] \
3271 : [::format %02d $stdMinute] \
3272 : [::format %02d $stdSecond]
3274 dict set WinZoneInfo $data $tzname
3277 return [dict get $WinZoneInfo $data]
3280 #----------------------------------------------------------------------
3282 # LoadTimeZoneFile --
3284 # Load the data file that specifies the conversion between a
3285 # given time zone and Greenwich.
3288 # fileName -- Name of the file to load
3294 # TZData(:fileName) contains the time zone data
3296 #----------------------------------------------------------------------
3298 proc ::tcl::clock::LoadTimeZoneFile { fileName } {
3302 if { [info exists TZData($fileName)] } {
3306 # Since an unsafe interp uses the [clock] command in the parent, this code
3307 # is security sensitive. Make sure that the path name cannot escape the
3310 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3311 return -code error \
3312 -errorcode [list CLOCK badTimeZone $:fileName] \
3313 "time zone \":$fileName\" not valid"
3316 source -encoding utf-8 [file join $DataDir $fileName]
3318 return -code error \
3319 -errorcode [list CLOCK badTimeZone :$fileName] \
3320 "time zone \":$fileName\" not found"
3325 #----------------------------------------------------------------------
3327 # LoadZoneinfoFile --
3329 # Loads a binary time zone information file in Olson format.
3332 # fileName - Relative path name of the file to load.
3335 # Returns an empty result normally; returns an error if no Olson file
3336 # was found or the file was malformed in some way.
3339 # TZData(:fileName) contains the time zone data
3341 #----------------------------------------------------------------------
3343 proc ::tcl::clock::LoadZoneinfoFile { fileName } {
3344 variable ZoneinfoPaths
3346 # Since an unsafe interp uses the [clock] command in the parent, this code
3347 # is security sensitive. Make sure that the path name cannot escape the
3350 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3351 return -code error \
3352 -errorcode [list CLOCK badTimeZone $:fileName] \
3353 "time zone \":$fileName\" not valid"
3355 foreach d $ZoneinfoPaths {
3356 set fname [file join $d $fileName]
3357 if { [file readable $fname] && [file isfile $fname] } {
3362 ReadZoneinfoFile $fileName $fname
3365 #----------------------------------------------------------------------
3367 # ReadZoneinfoFile --
3369 # Loads a binary time zone information file in Olson format.
3372 # fileName - Name of the time zone (relative path name of the
3374 # fname - Absolute path name of the file.
3377 # Returns an empty result normally; returns an error if no Olson file
3378 # was found or the file was malformed in some way.
3381 # TZData(:fileName) contains the time zone data
3383 #----------------------------------------------------------------------
3385 proc ::tcl::clock::ReadZoneinfoFile {fileName fname} {
3388 if { ![file exists $fname] } {
3389 return -code error "$fileName not found"
3392 if { [file size $fname] > 262144 } {
3393 return -code error "$fileName too big"
3396 # Suck in all the data from the file
3398 set f [open $fname r]
3399 fconfigure $f -translation binary
3403 # The file begins with a magic number, sixteen reserved bytes, and then
3404 # six 4-byte integers giving counts of fileds in the file.
3406 binary scan $d a4a1x15IIIIII \
3407 magic version nIsGMT nIsStd nLeap nTime nType nChar
3411 if { $magic != {TZif} } {
3412 return -code error "$fileName not a time zone information file"
3414 if { $nType > 255 } {
3415 return -code error "$fileName contains too many time types"
3417 # Accept only Posix-style zoneinfo. Sorry, 'leaps' bigots.
3418 if { $nLeap != 0 } {
3419 return -code error "$fileName contains leap seconds"
3422 # In a version 2 file, we use the second part of the file, which contains
3423 # 64-bit transition times.
3425 if {$version eq "2"} {
3435 binary scan $d @${seek}a4a1x15IIIIII \
3436 magic version nIsGMT nIsStd nLeap nTime nType nChar
3437 if {$magic ne {TZif}} {
3438 return -code error "seek address $seek miscomputed, magic = $magic"
3445 # Next come ${nTime} transition times, followed by ${nTime} time type
3446 # codes. The type codes are unsigned 1-byte quantities. We insert an
3447 # arbitrary start time in front of the transitions.
3449 binary scan $d @${seek}${iformat}${nTime}c${nTime} times tempCodes
3450 incr seek [expr { ($ilen + 1) * $nTime }]
3451 set times [linsert $times 0 $MINWIDE]
3453 foreach c $tempCodes {
3454 lappend codes [expr { $c & 0xFF }]
3456 set codes [linsert $codes 0 0]
3458 # Next come ${nType} time type descriptions, each of which has an offset
3459 # (seconds east of GMT), a DST indicator, and an index into the
3460 # abbreviation text.
3462 for { set i 0 } { $i < $nType } { incr i } {
3463 binary scan $d @${seek}Icc gmtOff isDst abbrInd
3464 lappend types [list $gmtOff $isDst $abbrInd]
3468 # Next come $nChar characters of time zone name abbreviations, which are
3470 # We build them up into a dictionary indexed by character index, because
3471 # that's what's in the indices above.
3473 binary scan $d @${seek}a${nChar} abbrs
3475 set abbrList [split $abbrs \0]
3478 foreach a $abbrList {
3479 for {set j 0} {$j <= [string length $a]} {incr j} {
3480 dict set abbrevs $i [string range $a $j end]
3485 # Package up a list of tuples, each of which contains transition time,
3486 # seconds east of Greenwich, DST flag and time zone abbreviation.
3489 set lastTime $MINWIDE
3490 foreach t $times c $codes {
3491 if { $t < $lastTime } {
3492 return -code error "$fileName has times out of order"
3495 lassign [lindex $types $c] gmtoff isDst abbrInd
3496 set abbrev [dict get $abbrevs $abbrInd]
3497 lappend r [list $t $gmtoff $isDst $abbrev]
3500 # In a version 2 file, there is also a POSIX-style time zone description
3501 # at the very end of the file. To get to it, skip over nLeap leap second
3502 # values (8 bytes each),
3503 # nIsStd standard/DST indicators and nIsGMT UTC/local indicators.
3505 if {$version eq {2}} {
3506 set seek [expr {$seek + 8 * $nLeap + $nIsStd + $nIsGMT + 1}]
3507 set last [string first \n $d $seek]
3508 set posix [string range $d $seek [expr {$last-1}]]
3509 if {[llength $posix] > 0} {
3510 set posixFields [ParsePosixTimeZone $posix]
3511 foreach tuple [ProcessPosixTimeZone $posixFields] {
3512 lassign $tuple t gmtoff isDst abbrev
3513 if {$t > $lastTime} {
3520 set TZData(:$fileName) $r
3525 #----------------------------------------------------------------------
3527 # ParsePosixTimeZone --
3529 # Parses the TZ environment variable in Posix form
3532 # tz Time zone specifier to be interpreted
3535 # Returns a dictionary whose values contain the various pieces of the
3536 # time zone specification.
3542 # Throws an error if the syntax of the time zone is incorrect.
3544 # The following keys are present in the dictionary:
3545 # stdName - Name of the time zone when Daylight Saving Time
3547 # stdSignum - Sign (+, -, or empty) of the offset from Greenwich
3548 # to the given (non-DST) time zone. + and the empty
3549 # string denote zones west of Greenwich, - denotes east
3550 # of Greenwich; this is contrary to the ISO convention
3551 # but follows Posix.
3552 # stdHours - Hours part of the offset from Greenwich to the given
3553 # (non-DST) time zone.
3554 # stdMinutes - Minutes part of the offset from Greenwich to the
3555 # given (non-DST) time zone. Empty denotes zero.
3556 # stdSeconds - Seconds part of the offset from Greenwich to the
3557 # given (non-DST) time zone. Empty denotes zero.
3558 # dstName - Name of the time zone when DST is in effect, or the
3559 # empty string if the time zone does not observe Daylight
3561 # dstSignum, dstHours, dstMinutes, dstSeconds -
3562 # Fields corresponding to stdSignum, stdHours, stdMinutes,
3563 # stdSeconds for the Daylight Saving Time version of the
3564 # time zone. If dstHours is empty, it is presumed to be 1.
3565 # startDayOfYear - The ordinal number of the day of the year on which
3566 # Daylight Saving Time begins. If this field is
3567 # empty, then DST begins on a given month-week-day,
3569 # startJ - The letter J, or an empty string. If a J is present in
3570 # this field, then startDayOfYear does not count February 29
3571 # even in leap years.
3572 # startMonth - The number of the month in which Daylight Saving Time
3573 # begins, supplied if startDayOfYear is empty. If both
3574 # startDayOfYear and startMonth are empty, then US rules
3576 # startWeekOfMonth - The number of the week in the month in which
3577 # Daylight Saving Time begins, in the range 1-5.
3578 # 5 denotes the last week of the month even in a
3580 # startDayOfWeek - The number of the day of the week (Sunday=0,
3581 # Saturday=6) on which Daylight Saving Time begins.
3582 # startHours - The hours part of the time of day at which Daylight
3583 # Saving Time begins. An empty string is presumed to be 2.
3584 # startMinutes - The minutes part of the time of day at which DST begins.
3585 # An empty string is presumed zero.
3586 # startSeconds - The seconds part of the time of day at which DST begins.
3587 # An empty string is presumed zero.
3588 # endDayOfYear, endJ, endMonth, endWeekOfMonth, endDayOfWeek,
3589 # endHours, endMinutes, endSeconds -
3590 # Specify the end of DST in the same way that the start* fields
3591 # specify the beginning of DST.
3593 # This procedure serves only to break the time specifier into fields. No
3594 # attempt is made to canonicalize the fields or supply default values.
3596 #----------------------------------------------------------------------
3598 proc ::tcl::clock::ParsePosixTimeZone { tz } {
3599 if {[regexp -expanded -nocase -- {
3601 # 1 - Standard time zone name
3602 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3603 # 2 - Standard time zone offset, signum
3605 # 3 - Standard time zone offset, hours
3608 # 4 - Standard time zone offset, minutes
3609 : ([[:digit:]]{1,2})
3611 # 5 - Standard time zone offset, seconds
3612 : ([[:digit:]]{1,2} )
3616 # 6 - DST time zone name
3617 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3620 # 7 - DST time zone offset, signum
3622 # 8 - DST time zone offset, hours
3625 # 9 - DST time zone offset, minutes
3626 : ([[:digit:]]{1,2})
3628 # 10 - DST time zone offset, seconds
3629 : ([[:digit:]]{1,2})
3636 # 11 - Optional J in n and Jn form 12 - Day of year
3637 ( J ? ) ( [[:digit:]]+ )
3639 # 13 - Month number 14 - Week of month 15 - Day of week
3641 [.] ( [[:digit:]] + )
3642 [.] ( [[:digit:]] + )
3645 # 16 - Start time of DST - hours
3646 / ( [[:digit:]]{1,2} )
3648 # 17 - Start time of DST - minutes
3649 : ( [[:digit:]]{1,2} )
3651 # 18 - Start time of DST - seconds
3652 : ( [[:digit:]]{1,2} )
3658 # 19 - Optional J in n and Jn form 20 - Day of year
3659 ( J ? ) ( [[:digit:]]+ )
3661 # 21 - Month number 22 - Week of month 23 - Day of week
3663 [.] ( [[:digit:]] + )
3664 [.] ( [[:digit:]] + )
3667 # 24 - End time of DST - hours
3668 / ( [[:digit:]]{1,2} )
3670 # 25 - End time of DST - minutes
3671 : ( [[:digit:]]{1,2} )
3673 # 26 - End time of DST - seconds
3674 : ( [[:digit:]]{1,2} )
3682 } $tz -> x(stdName) x(stdSignum) x(stdHours) x(stdMinutes) x(stdSeconds) \
3683 x(dstName) x(dstSignum) x(dstHours) x(dstMinutes) x(dstSeconds) \
3684 x(startJ) x(startDayOfYear) \
3685 x(startMonth) x(startWeekOfMonth) x(startDayOfWeek) \
3686 x(startHours) x(startMinutes) x(startSeconds) \
3687 x(endJ) x(endDayOfYear) \
3688 x(endMonth) x(endWeekOfMonth) x(endDayOfWeek) \
3689 x(endHours) x(endMinutes) x(endSeconds)] } {
3690 # it's a good timezone
3692 return [array get x]
3696 -errorcode [list CLOCK badTimeZone $tz] \
3697 "unable to parse time zone specification \"$tz\""
3700 #----------------------------------------------------------------------
3702 # ProcessPosixTimeZone --
3704 # Handle a Posix time zone after it's been broken out into fields.
3707 # z - Dictionary returned from 'ParsePosixTimeZone'
3710 # Returns time zone information for the 'TZData' array.
3715 #----------------------------------------------------------------------
3717 proc ::tcl::clock::ProcessPosixTimeZone { z } {
3721 # Determine the standard time zone name and seconds east of Greenwich
3723 set stdName [dict get $z stdName]
3724 if { [string index $stdName 0] eq {<} } {
3725 set stdName [string range $stdName 1 end-1]
3727 if { [dict get $z stdSignum] eq {-} } {
3732 set stdHours [lindex [::scan [dict get $z stdHours] %d] 0]
3733 if { [dict get $z stdMinutes] ne {} } {
3734 set stdMinutes [lindex [::scan [dict get $z stdMinutes] %d] 0]
3738 if { [dict get $z stdSeconds] ne {} } {
3739 set stdSeconds [lindex [::scan [dict get $z stdSeconds] %d] 0]
3743 set stdOffset [expr {
3744 (($stdHours * 60 + $stdMinutes) * 60 + $stdSeconds) * $stdSignum
3746 set data [list [list $MINWIDE $stdOffset 0 $stdName]]
3748 # If there's no daylight zone, we're done
3750 set dstName [dict get $z dstName]
3751 if { $dstName eq {} } {
3754 if { [string index $dstName 0] eq {<} } {
3755 set dstName [string range $dstName 1 end-1]
3758 # Determine the daylight name
3760 if { [dict get $z dstSignum] eq {-} } {
3765 if { [dict get $z dstHours] eq {} } {
3766 set dstOffset [expr { 3600 + $stdOffset }]
3768 set dstHours [lindex [::scan [dict get $z dstHours] %d] 0]
3769 if { [dict get $z dstMinutes] ne {} } {
3770 set dstMinutes [lindex [::scan [dict get $z dstMinutes] %d] 0]
3774 if { [dict get $z dstSeconds] ne {} } {
3775 set dstSeconds [lindex [::scan [dict get $z dstSeconds] %d] 0]
3779 set dstOffset [expr {
3780 (($dstHours*60 + $dstMinutes) * 60 + $dstSeconds) * $dstSignum
3784 # Fill in defaults for European or US DST rules
3785 # US start time is the second Sunday in March
3786 # EU start time is the last Sunday in March
3787 # US end time is the first Sunday in November.
3788 # EU end time is the last Sunday in October
3791 [dict get $z startDayOfYear] eq {}
3792 && [dict get $z startMonth] eq {}
3794 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3796 dict set z startWeekOfMonth 5
3798 dict set z startHours 2
3800 dict set z startHours [expr {$stdHours+1}]
3804 dict set z startWeekOfMonth 2
3805 dict set z startHours 2
3807 dict set z startMonth 3
3808 dict set z startDayOfWeek 0
3809 dict set z startMinutes 0
3810 dict set z startSeconds 0
3813 [dict get $z endDayOfYear] eq {}
3814 && [dict get $z endMonth] eq {}
3816 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3818 dict set z endMonth 10
3819 dict set z endWeekOfMonth 5
3821 dict set z endHours 3
3823 dict set z endHours [expr {$stdHours+2}]
3827 dict set z endMonth 11
3828 dict set z endWeekOfMonth 1
3829 dict set z endHours 2
3831 dict set z endDayOfWeek 0
3832 dict set z endMinutes 0
3833 dict set z endSeconds 0
3836 # Put DST in effect in all years from 1916 to 2099.
3838 for { set y 1916 } { $y < 2100 } { incr y } {
3839 set startTime [DeterminePosixDSTTime $z start $y]
3840 incr startTime [expr { - wide($stdOffset) }]
3841 set endTime [DeterminePosixDSTTime $z end $y]
3842 incr endTime [expr { - wide($dstOffset) }]
3843 if { $startTime < $endTime } {
3845 [list $startTime $dstOffset 1 $dstName] \
3846 [list $endTime $stdOffset 0 $stdName]
3849 [list $endTime $stdOffset 0 $stdName] \
3850 [list $startTime $dstOffset 1 $dstName]
3857 #----------------------------------------------------------------------
3859 # DeterminePosixDSTTime --
3861 # Determines the time that Daylight Saving Time starts or ends from a
3862 # Posix time zone specification.
3865 # z - Time zone data returned from ParsePosixTimeZone.
3866 # Missing fields are expected to be filled in with
3868 # bound - The word 'start' or 'end'
3869 # y - The year for which the transition time is to be determined.
3872 # Returns the transition time as a count of seconds from the epoch. The
3873 # time is relative to the wall clock, not UTC.
3875 #----------------------------------------------------------------------
3877 proc ::tcl::clock::DeterminePosixDSTTime { z bound y } {
3881 # Determine the start or end day of DST
3883 set date [dict create era CE year $y]
3884 set doy [dict get $z ${bound}DayOfYear]
3887 # Time was specified as a day of the year
3889 if { [dict get $z ${bound}J] ne {}
3890 && [IsGregorianLeapYear $y]
3891 && ( $doy > $FEB_28 ) } {
3894 dict set date dayOfYear $doy
3895 set date [GetJulianDayFromEraYearDay $date[set date {}] 2361222]
3897 # Time was specified as a day of the week within a month
3899 dict set date month [dict get $z ${bound}Month]
3900 dict set date dayOfWeek [dict get $z ${bound}DayOfWeek]
3901 set dowim [dict get $z ${bound}WeekOfMonth]
3902 if { $dowim >= 5 } {
3905 dict set date dayOfWeekInMonth $dowim
3906 set date [GetJulianDayFromEraYearMonthWeekDay $date[set date {}] 2361222]
3910 set jd [dict get $date julianDay]
3912 wide($jd) * wide(86400) - wide(210866803200)
3915 set h [dict get $z ${bound}Hours]
3919 set h [lindex [::scan $h %d] 0]
3921 set m [dict get $z ${bound}Minutes]
3925 set m [lindex [::scan $m %d] 0]
3927 set s [dict get $z ${bound}Seconds]
3931 set s [lindex [::scan $s %d] 0]
3933 set tod [expr { ( $h * 60 + $m ) * 60 + $s }]
3934 return [expr { $seconds + $tod }]
3937 #----------------------------------------------------------------------
3941 # Given local time expressed in seconds from the Posix epoch,
3942 # determine localized era and year within the era.
3945 # date - Dictionary that must contain the keys, 'localSeconds',
3946 # whose value is expressed as the appropriate local time;
3947 # and 'year', whose value is the Gregorian year.
3948 # etable - Value of the LOCALE_ERAS key in the message catalogue
3949 # for the target locale.
3952 # Returns the dictionary, augmented with the keys, 'localeEra' and
3955 #----------------------------------------------------------------------
3957 proc ::tcl::clock::GetLocaleEra { date etable } {
3958 set index [BSearch $etable [dict get $date localSeconds]]
3960 dict set date localeEra \
3961 [::format %02d [expr { [dict get $date year] / 100 }]]
3962 dict set date localeYear [expr {
3963 [dict get $date year] % 100
3966 dict set date localeEra [lindex $etable $index 1]
3967 dict set date localeYear [expr {
3968 [dict get $date year] - [lindex $etable $index 2]
3974 #----------------------------------------------------------------------
3976 # GetJulianDayFromEraYearDay --
3978 # Given a year, month and day on the Gregorian calendar, determines
3979 # the Julian Day Number beginning at noon on that date.
3982 # date -- A dictionary in which the 'era', 'year', and
3983 # 'dayOfYear' slots are populated. The calendar in use
3984 # is determined by the date itself relative to:
3985 # changeover -- Julian day on which the Gregorian calendar was
3986 # adopted in the current locale.
3989 # Returns the given dictionary augmented with a 'julianDay' key whose
3990 # value is the desired Julian Day Number, and a 'gregorian' key that
3991 # specifies whether the calendar is Gregorian (1) or Julian (0).
3997 # This code needs to be moved to the C layer.
3999 #----------------------------------------------------------------------
4001 proc ::tcl::clock::GetJulianDayFromEraYearDay {date changeover} {
4002 # Get absolute year number from the civil year
4004 switch -exact -- [dict get $date era] {
4006 set year [expr { 1 - [dict get $date year] }]
4009 set year [dict get $date year]
4012 set ym1 [expr { $year - 1 }]
4014 # Try the Gregorian calendar first.
4016 dict set date gregorian 1
4019 + [dict get $date dayOfYear]
4026 # If the date is before the Gregorian change, use the Julian calendar.
4028 if { $jd < $changeover } {
4029 dict set date gregorian 0
4032 + [dict get $date dayOfYear]
4038 dict set date julianDay $jd
4042 #----------------------------------------------------------------------
4044 # GetJulianDayFromEraYearMonthWeekDay --
4046 # Determines the Julian Day number corresponding to the nth given
4047 # day-of-the-week in a given month.
4050 # date - Dictionary containing the keys, 'era', 'year', 'month'
4051 # 'weekOfMonth', 'dayOfWeek', and 'dayOfWeekInMonth'.
4052 # changeover - Julian Day of adoption of the Gregorian calendar
4055 # Returns the given dictionary, augmented with a 'julianDay' key.
4061 # This code needs to be moved to the C layer.
4063 #----------------------------------------------------------------------
4065 proc ::tcl::clock::GetJulianDayFromEraYearMonthWeekDay {date changeover} {
4066 # Come up with a reference day; either the zeroeth day of the given month
4067 # (dayOfWeekInMonth >= 0) or the seventh day of the following month
4068 # (dayOfWeekInMonth < 0)
4071 set week [dict get $date dayOfWeekInMonth]
4073 dict set date2 dayOfMonth 0
4075 dict incr date2 month
4076 dict set date2 dayOfMonth 7
4078 set date2 [GetJulianDayFromEraYearMonthDay $date2[set date2 {}] \
4080 set wd0 [WeekdayOnOrBefore [dict get $date dayOfWeek] \
4081 [dict get $date2 julianDay]]
4082 dict set date julianDay [expr { $wd0 + 7 * $week }]
4086 #----------------------------------------------------------------------
4088 # IsGregorianLeapYear --
4090 # Determines whether a given date represents a leap year in the
4091 # Gregorian calendar.
4094 # date -- The date to test. The fields, 'era', 'year' and 'gregorian'
4098 # Returns 1 if the year is a leap year, 0 otherwise.
4103 #----------------------------------------------------------------------
4105 proc ::tcl::clock::IsGregorianLeapYear { date } {
4106 switch -exact -- [dict get $date era] {
4108 set year [expr { 1 - [dict get $date year]}]
4111 set year [dict get $date year]
4114 if { $year % 4 != 0 } {
4116 } elseif { ![dict get $date gregorian] } {
4118 } elseif { $year % 400 == 0 } {
4120 } elseif { $year % 100 == 0 } {
4127 #----------------------------------------------------------------------
4129 # WeekdayOnOrBefore --
4131 # Determine the nearest day of week (given by the 'weekday' parameter,
4132 # Sunday==0) on or before a given Julian Day.
4135 # weekday -- Day of the week
4136 # j -- Julian Day number
4139 # Returns the Julian Day Number of the desired date.
4144 #----------------------------------------------------------------------
4146 proc ::tcl::clock::WeekdayOnOrBefore { weekday j } {
4147 set k [expr { ( $weekday + 6 ) % 7 }]
4148 return [expr { $j - ( $j - $k ) % 7 }]
4151 #----------------------------------------------------------------------
4155 # Service procedure that does binary search in several places inside the
4159 # list - List of lists, sorted in ascending order by the
4161 # key - Value to search for
4164 # Returns the index of the greatest element in $list that is less than
4170 #----------------------------------------------------------------------
4172 proc ::tcl::clock::BSearch { list key } {
4173 if {[llength $list] == 0} {
4176 if { $key < [lindex $list 0 0] } {
4181 set u [expr { [llength $list] - 1 }]
4184 # At this point, we know that
4185 # $k >= [lindex $list $l 0]
4186 # Either $u == [llength $list] or else $k < [lindex $list $u+1 0]
4187 # We find the midpoint of the interval {l,u} rounded UP, compare
4188 # against it, and set l or u to maintain the invariant. Note that the
4189 # interval shrinks at each step, guaranteeing convergence.
4191 set m [expr { ( $l + $u + 1 ) / 2 }]
4192 if { $key >= [lindex $list $m 0] } {
4195 set u [expr { $m - 1 }]
4202 #----------------------------------------------------------------------
4206 # Adds an offset to a given time.
4209 # clock add clockval ?count unit?... ?-option value?
4212 # clockval -- Starting time value
4213 # count -- Amount of a unit of time to add
4214 # unit -- Unit of time to add, must be one of:
4215 # years year months month weeks week
4216 # days day hours hour minutes minute
4221 # (Deprecated) Flag synonymous with '-timezone :GMT'
4223 # Name of the time zone in which calculations are to be done.
4225 # Name of the locale in which calculations are to be done.
4226 # Used to determine the Gregorian change date.
4229 # Returns the given time adjusted by the given offset(s) in
4233 # It is possible that adding a number of months or years will adjust the
4234 # day of the month as well. For instance, the time at one month after
4235 # 31 January is either 28 or 29 February, because February has fewer
4238 #----------------------------------------------------------------------
4240 proc ::tcl::clock::add { clockval args } {
4241 if { [llength $args] % 2 != 0 } {
4242 set cmdName "clock add"
4243 return -code error \
4244 -errorcode [list CLOCK wrongNumArgs] \
4245 "wrong \# args: should be\
4246 \"$cmdName clockval ?number units?...\
4247 ?-gmt boolean? ?-locale LOCALE? ?-timezone ZONE?\""
4249 if { [catch { expr {wide($clockval)} } result] } {
4250 return -code error $result
4256 set timezone [GetSystemTimeZone]
4258 foreach { a b } $args {
4259 if { [string is integer -strict $a] } {
4260 lappend offsets $a $b
4262 switch -exact -- $a {
4266 -l - -lo - -loc - -loca - -local - -locale {
4267 set locale [string tolower $b]
4269 -t - -ti - -tim - -time - -timez - -timezo - -timezon -
4274 throw [list CLOCK badOption $a] \
4275 "bad option \"$a\",\
4276 must be -gmt, -locale or -timezone"
4282 # Check options for validity
4284 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
4285 return -code error \
4286 -errorcode [list CLOCK gmtWithTimezone] \
4287 "cannot use -gmt and -timezone in same call"
4289 if { [catch { expr { wide($clockval) } } result] } {
4290 return -code error "expected integer but got \"$clockval\""
4292 if { ![string is boolean -strict $gmt] } {
4293 return -code error "expected boolean value but got \"$gmt\""
4300 set changeover [mc GREGORIAN_CHANGE_DATE]
4302 if {[catch {SetupTimeZone $timezone} retval opts]} {
4303 dict unset opts -errorinfo
4304 return -options $opts $retval
4308 foreach { quantity unit } $offsets {
4309 switch -exact -- $unit {
4311 set clockval [AddMonths [expr { 12 * $quantity }] \
4312 $clockval $timezone $changeover]
4315 set clockval [AddMonths $quantity $clockval $timezone \
4320 set clockval [AddDays [expr { 7 * $quantity }] \
4321 $clockval $timezone $changeover]
4324 set clockval [AddDays $quantity $clockval $timezone \
4329 set clockval [expr { 3600 * $quantity + $clockval }]
4332 set clockval [expr { 60 * $quantity + $clockval }]
4335 set clockval [expr { $quantity + $clockval }]
4339 throw [list CLOCK badUnit $unit] \
4340 "unknown unit \"$unit\", must be \
4341 years, months, weeks, days, hours, minutes or seconds"
4346 } trap CLOCK {result opts} {
4347 # Conceal the innards of [clock] when it's an expected error
4348 dict unset opts -errorinfo
4349 return -options $opts $result
4353 #----------------------------------------------------------------------
4357 # Add a given number of months to a given clock value in a given
4361 # months - Number of months to add (may be negative)
4362 # clockval - Seconds since the epoch before the operation
4363 # timezone - Time zone in which the operation is to be performed
4366 # Returns the new clock value as a number of seconds since
4372 #----------------------------------------------------------------------
4374 proc ::tcl::clock::AddMonths { months clockval timezone changeover } {
4375 variable DaysInRomanMonthInCommonYear
4376 variable DaysInRomanMonthInLeapYear
4379 # Convert the time to year, month, day, and fraction of day.
4381 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4382 dict set date secondOfDay [expr {
4383 [dict get $date localSeconds] % 86400
4385 dict set date tzName $timezone
4387 # Add the requisite number of months
4389 set m [dict get $date month]
4392 set delta [expr { $m / 12 }]
4393 set mm [expr { $m % 12 }]
4394 dict set date month [expr { $mm + 1 }]
4395 dict incr date year $delta
4397 # If the date doesn't exist in the current month, repair it
4399 if { [IsGregorianLeapYear $date] } {
4400 set hath [lindex $DaysInRomanMonthInLeapYear $mm]
4402 set hath [lindex $DaysInRomanMonthInCommonYear $mm]
4404 if { [dict get $date dayOfMonth] > $hath } {
4405 dict set date dayOfMonth $hath
4408 # Reconvert to a number of seconds
4410 set date [GetJulianDayFromEraYearMonthDay \
4413 dict set date localSeconds [expr {
4415 + ( 86400 * wide([dict get $date julianDay]) )
4416 + [dict get $date secondOfDay]
4418 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4421 return [dict get $date seconds]
4425 #----------------------------------------------------------------------
4429 # Add a given number of days to a given clock value in a given time
4433 # days - Number of days to add (may be negative)
4434 # clockval - Seconds since the epoch before the operation
4435 # timezone - Time zone in which the operation is to be performed
4436 # changeover - Julian Day on which the Gregorian calendar was adopted
4437 # in the target locale.
4440 # Returns the new clock value as a number of seconds since the epoch.
4445 #----------------------------------------------------------------------
4447 proc ::tcl::clock::AddDays { days clockval timezone changeover } {
4450 # Convert the time to Julian Day
4452 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4453 dict set date secondOfDay [expr {
4454 [dict get $date localSeconds] % 86400
4456 dict set date tzName $timezone
4458 # Add the requisite number of days
4460 dict incr date julianDay $days
4462 # Reconvert to a number of seconds
4464 dict set date localSeconds [expr {
4466 + ( 86400 * wide([dict get $date julianDay]) )
4467 + [dict get $date secondOfDay]
4469 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4472 return [dict get $date seconds]
4476 #----------------------------------------------------------------------
4478 # ChangeCurrentLocale --
4480 # The global locale was changed within msgcat.
4481 # Clears the buffered parse functions of the current locale.
4490 # Buffered parse functions are cleared.
4492 #----------------------------------------------------------------------
4494 proc ::tcl::clock::ChangeCurrentLocale {args} {
4496 variable LocaleNumeralCache
4497 variable CachedSystemTimeZone
4498 variable TimeZoneBad
4500 foreach p [info procs [namespace current]::scanproc'*'current] {
4503 foreach p [info procs [namespace current]::formatproc'*'current] {
4507 catch {array unset FormatProc *'current}
4508 set LocaleNumeralCache {}
4511 #----------------------------------------------------------------------
4515 # Clears all caches to reclaim the memory used in [clock]
4524 # Caches are cleared.
4526 #----------------------------------------------------------------------
4528 proc ::tcl::clock::ClearCaches {} {
4530 variable LocaleNumeralCache
4531 variable CachedSystemTimeZone
4532 variable TimeZoneBad
4534 foreach p [info procs [namespace current]::scanproc'*] {
4537 foreach p [info procs [namespace current]::formatproc'*] {
4541 catch {unset FormatProc}
4542 set LocaleNumeralCache {}
4543 catch {unset CachedSystemTimeZone}