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,2005,2006,2007 by Kevin B. Kenny
13 # See the file "license.terms" for information on usage and redistribution
14 # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
16 #----------------------------------------------------------------------
18 # We must have message catalogs that support the root locale, and 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 if {![info exists timezone]} {
2993 # Cache the time zone only if it was detected by one of the
2994 # expensive methods.
2995 if { [info exists CachedSystemTimeZone] } {
2996 set timezone $CachedSystemTimeZone
2997 } elseif { $::tcl_platform(platform) eq {windows} } {
2998 set timezone [GuessWindowsTimeZone]
2999 } elseif { [file exists /etc/localtime]
3000 && ![catch {ReadZoneinfoFile \
3001 Tcl/Localtime /etc/localtime}] } {
3002 set timezone :Tcl/Localtime
3004 set timezone :localtime
3006 set CachedSystemTimeZone $timezone
3008 if { ![dict exists $TimeZoneBad $timezone] } {
3009 dict set TimeZoneBad $timezone [catch {SetupTimeZone $timezone}]
3011 if { [dict get $TimeZoneBad $timezone] } {
3018 #----------------------------------------------------------------------
3020 # ConvertLegacyTimeZone --
3022 # Given an alphanumeric time zone identifier and the system time zone,
3023 # convert the alphanumeric identifier to an unambiguous time zone.
3026 # tzname - Name of the time zone to convert
3029 # Returns a time zone name corresponding to tzname, but in an
3030 # unambiguous form, generally +hhmm.
3032 # This procedure is implemented primarily to allow the parsing of RFC822
3033 # date/time strings. Processing a time zone name on input is not recommended
3034 # practice, because there is considerable room for ambiguity; for instance, is
3035 # BST Brazilian Standard Time, or British Summer Time?
3037 #----------------------------------------------------------------------
3039 proc ::tcl::clock::ConvertLegacyTimeZone { tzname } {
3040 variable LegacyTimeZone
3042 set tzname [string tolower $tzname]
3043 if { ![dict exists $LegacyTimeZone $tzname] } {
3044 return -code error -errorcode [list CLOCK badTZName $tzname] \
3045 "time zone \"$tzname\" not found"
3047 return [dict get $LegacyTimeZone $tzname]
3050 #----------------------------------------------------------------------
3054 # Given the name or specification of a time zone, sets up its in-memory
3058 # tzname - Name of a time zone
3061 # Unless the time zone is ':localtime', sets the TZData array to contain
3062 # the lookup table for local<->UTC conversion. Returns an error if the
3063 # time zone cannot be parsed.
3065 #----------------------------------------------------------------------
3067 proc ::tcl::clock::SetupTimeZone { timezone } {
3070 if {! [info exists TZData($timezone)] } {
3072 if { $timezone eq {:localtime} } {
3073 # Nothing to do, we'll convert using the localtime function
3076 [regexp {^([-+])(\d\d)(?::?(\d\d)(?::?(\d\d))?)?} $timezone \
3079 # Make a fixed offset
3092 set offset [expr { ( $hh * 60 + $mm ) * 60 + $ss }]
3094 set offset [expr { - $offset }]
3096 set TZData($timezone) [list [list $MINWIDE $offset -1 $timezone]]
3098 } elseif { [string index $timezone 0] eq {:} } {
3099 # Convert using a time zone file
3103 LoadTimeZoneFile [string range $timezone 1 end]
3105 LoadZoneinfoFile [string range $timezone 1 end]
3108 return -code error \
3109 -errorcode [list CLOCK badTimeZone $timezone] \
3110 "time zone \"$timezone\" not found"
3112 } elseif { ![catch {ParsePosixTimeZone $timezone} tzfields] } {
3113 # This looks like a POSIX time zone - try to process it
3115 if { [catch {ProcessPosixTimeZone $tzfields} data opts] } {
3116 if { [lindex [dict get $opts -errorcode] 0] eq {CLOCK} } {
3117 dict unset opts -errorinfo
3119 return -options $opts $data
3121 set TZData($timezone) $data
3125 # We couldn't parse this as a POSIX time zone. Try again with a
3126 # time zone file - this time without a colon
3128 if { [catch { LoadTimeZoneFile $timezone }]
3129 && [catch { LoadZoneinfoFile $timezone } - opts] } {
3130 dict unset opts -errorinfo
3131 return -options $opts "time zone $timezone not found"
3133 set TZData($timezone) $TZData(:$timezone)
3140 #----------------------------------------------------------------------
3142 # GuessWindowsTimeZone --
3144 # Determines the system time zone on windows.
3150 # Returns a time zone specifier that corresponds to the system time zone
3151 # information found in the Registry.
3154 # Fixed dates for DST change are unimplemented at present, because no
3155 # time zone information supplied with Windows actually uses them!
3157 # On a Windows system where neither $env(TCL_TZ) nor $env(TZ) is specified,
3158 # GuessWindowsTimeZone looks in the Registry for the system time zone
3159 # information. It then attempts to find an entry in WinZoneInfo for a time
3160 # zone that uses the same rules. If it finds one, it returns it; otherwise,
3161 # it constructs a Posix-style time zone string and returns that.
3163 #----------------------------------------------------------------------
3165 proc ::tcl::clock::GuessWindowsTimeZone {} {
3166 variable WinZoneInfo
3168 variable TimeZoneBad
3170 if { [info exists NoRegistry] } {
3174 # Dredge time zone information out of the registry
3177 set rpath HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\TimeZoneInformation
3180 * [registry get $rpath Bias] }] \
3182 * [registry get $rpath StandardBias] }] \
3184 * [registry get $rpath DaylightBias] }]]
3185 set stdtzi [registry get $rpath StandardStart]
3186 foreach ind {0 2 14 4 6 8 10 12} {
3187 binary scan $stdtzi @${ind}s val
3190 set daytzi [registry get $rpath DaylightStart]
3191 foreach ind {0 2 14 4 6 8 10 12} {
3192 binary scan $daytzi @${ind}s val
3196 # Missing values in the Registry - bail out
3201 # Make up a Posix time zone specifier if we can't find one. Check here
3202 # that the tzdata file exists, in case we're running in an environment
3203 # (e.g. starpack) where tzdata is incomplete. (Bug 1237907)
3205 if { [dict exists $WinZoneInfo $data] } {
3206 set tzname [dict get $WinZoneInfo $data]
3207 if { ! [dict exists $TimeZoneBad $tzname] } {
3208 dict set TimeZoneBad $tzname [catch {SetupTimeZone $tzname}]
3213 if { $tzname eq {} || [dict get $TimeZoneBad $tzname] } {
3215 bias stdBias dstBias \
3216 stdYear stdMonth stdDayOfWeek stdDayOfMonth \
3217 stdHour stdMinute stdSecond stdMillisec \
3218 dstYear dstMonth dstDayOfWeek dstDayOfMonth \
3219 dstHour dstMinute dstSecond dstMillisec
3220 set stdDelta [expr { $bias + $stdBias }]
3221 set dstDelta [expr { $bias + $dstBias }]
3222 if { $stdDelta <= 0 } {
3224 set stdDelta [expr { - $stdDelta }]
3230 set hh [::format %02d [expr { $stdDelta / 3600 }]]
3231 set mm [::format %02d [expr { ($stdDelta / 60 ) % 60 }]]
3232 set ss [::format %02d [expr { $stdDelta % 60 }]]
3234 append tzname < $dispStdSignum $hh $mm > $stdSignum $hh : $mm : $ss
3235 if { $stdMonth >= 0 } {
3236 if { $dstDelta <= 0 } {
3238 set dstDelta [expr { - $dstDelta }]
3244 set hh [::format %02d [expr { $dstDelta / 3600 }]]
3245 set mm [::format %02d [expr { ($dstDelta / 60 ) % 60 }]]
3246 set ss [::format %02d [expr { $dstDelta % 60 }]]
3247 append tzname < $dispDstSignum $hh $mm > $dstSignum $hh : $mm : $ss
3248 if { $dstYear == 0 } {
3249 append tzname ,M $dstMonth . $dstDayOfMonth . $dstDayOfWeek
3251 # I have not been able to find any locale on which Windows
3252 # converts time zone on a fixed day of the year, hence don't
3253 # know how to interpret the fields. If someone can inform me,
3254 # I'd be glad to code it up. For right now, we bail out in
3258 append tzname / [::format %02d $dstHour] \
3259 : [::format %02d $dstMinute] \
3260 : [::format %02d $dstSecond]
3261 if { $stdYear == 0 } {
3262 append tzname ,M $stdMonth . $stdDayOfMonth . $stdDayOfWeek
3264 # I have not been able to find any locale on which Windows
3265 # converts time zone on a fixed day of the year, hence don't
3266 # know how to interpret the fields. If someone can inform me,
3267 # I'd be glad to code it up. For right now, we bail out in
3271 append tzname / [::format %02d $stdHour] \
3272 : [::format %02d $stdMinute] \
3273 : [::format %02d $stdSecond]
3275 dict set WinZoneInfo $data $tzname
3278 return [dict get $WinZoneInfo $data]
3281 #----------------------------------------------------------------------
3283 # LoadTimeZoneFile --
3285 # Load the data file that specifies the conversion between a
3286 # given time zone and Greenwich.
3289 # fileName -- Name of the file to load
3295 # TZData(:fileName) contains the time zone data
3297 #----------------------------------------------------------------------
3299 proc ::tcl::clock::LoadTimeZoneFile { fileName } {
3303 if { [info exists TZData($fileName)] } {
3307 # Since an unsafe interp uses the [clock] command in the master, this code
3308 # is security sensitive. Make sure that the path name cannot escape the
3311 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3312 return -code error \
3313 -errorcode [list CLOCK badTimeZone $:fileName] \
3314 "time zone \":$fileName\" not valid"
3317 source -encoding utf-8 [file join $DataDir $fileName]
3319 return -code error \
3320 -errorcode [list CLOCK badTimeZone :$fileName] \
3321 "time zone \":$fileName\" not found"
3326 #----------------------------------------------------------------------
3328 # LoadZoneinfoFile --
3330 # Loads a binary time zone information file in Olson format.
3333 # fileName - Relative path name of the file to load.
3336 # Returns an empty result normally; returns an error if no Olson file
3337 # was found or the file was malformed in some way.
3340 # TZData(:fileName) contains the time zone data
3342 #----------------------------------------------------------------------
3344 proc ::tcl::clock::LoadZoneinfoFile { fileName } {
3345 variable ZoneinfoPaths
3347 # Since an unsafe interp uses the [clock] command in the master, this code
3348 # is security sensitive. Make sure that the path name cannot escape the
3351 if { ![regexp {^[[.-.][:alpha:]_]+(?:/[[.-.][:alpha:]_]+)*$} $fileName] } {
3352 return -code error \
3353 -errorcode [list CLOCK badTimeZone $:fileName] \
3354 "time zone \":$fileName\" not valid"
3356 foreach d $ZoneinfoPaths {
3357 set fname [file join $d $fileName]
3358 if { [file readable $fname] && [file isfile $fname] } {
3363 ReadZoneinfoFile $fileName $fname
3366 #----------------------------------------------------------------------
3368 # ReadZoneinfoFile --
3370 # Loads a binary time zone information file in Olson format.
3373 # fileName - Name of the time zone (relative path name of the
3375 # fname - Absolute path name of the file.
3378 # Returns an empty result normally; returns an error if no Olson file
3379 # was found or the file was malformed in some way.
3382 # TZData(:fileName) contains the time zone data
3384 #----------------------------------------------------------------------
3386 proc ::tcl::clock::ReadZoneinfoFile {fileName fname} {
3389 if { ![file exists $fname] } {
3390 return -code error "$fileName not found"
3393 if { [file size $fname] > 262144 } {
3394 return -code error "$fileName too big"
3397 # Suck in all the data from the file
3399 set f [open $fname r]
3400 fconfigure $f -translation binary
3404 # The file begins with a magic number, sixteen reserved bytes, and then
3405 # six 4-byte integers giving counts of fileds in the file.
3407 binary scan $d a4a1x15IIIIII \
3408 magic version nIsGMT nIsStd nLeap nTime nType nChar
3412 if { $magic != {TZif} } {
3413 return -code error "$fileName not a time zone information file"
3415 if { $nType > 255 } {
3416 return -code error "$fileName contains too many time types"
3418 # Accept only Posix-style zoneinfo. Sorry, 'leaps' bigots.
3419 if { $nLeap != 0 } {
3420 return -code error "$fileName contains leap seconds"
3423 # In a version 2 file, we use the second part of the file, which contains
3424 # 64-bit transition times.
3426 if {$version eq "2"} {
3436 binary scan $d @${seek}a4a1x15IIIIII \
3437 magic version nIsGMT nIsStd nLeap nTime nType nChar
3438 if {$magic ne {TZif}} {
3439 return -code error "seek address $seek miscomputed, magic = $magic"
3446 # Next come ${nTime} transition times, followed by ${nTime} time type
3447 # codes. The type codes are unsigned 1-byte quantities. We insert an
3448 # arbitrary start time in front of the transitions.
3450 binary scan $d @${seek}${iformat}${nTime}c${nTime} times tempCodes
3451 incr seek [expr { ($ilen + 1) * $nTime }]
3452 set times [linsert $times 0 $MINWIDE]
3454 foreach c $tempCodes {
3455 lappend codes [expr { $c & 0xff }]
3457 set codes [linsert $codes 0 0]
3459 # Next come ${nType} time type descriptions, each of which has an offset
3460 # (seconds east of GMT), a DST indicator, and an index into the
3461 # abbreviation text.
3463 for { set i 0 } { $i < $nType } { incr i } {
3464 binary scan $d @${seek}Icc gmtOff isDst abbrInd
3465 lappend types [list $gmtOff $isDst $abbrInd]
3469 # Next come $nChar characters of time zone name abbreviations, which are
3471 # We build them up into a dictionary indexed by character index, because
3472 # that's what's in the indices above.
3474 binary scan $d @${seek}a${nChar} abbrs
3476 set abbrList [split $abbrs \0]
3479 foreach a $abbrList {
3480 for {set j 0} {$j <= [string length $a]} {incr j} {
3481 dict set abbrevs $i [string range $a $j end]
3486 # Package up a list of tuples, each of which contains transition time,
3487 # seconds east of Greenwich, DST flag and time zone abbreviation.
3490 set lastTime $MINWIDE
3491 foreach t $times c $codes {
3492 if { $t < $lastTime } {
3493 return -code error "$fileName has times out of order"
3496 lassign [lindex $types $c] gmtoff isDst abbrInd
3497 set abbrev [dict get $abbrevs $abbrInd]
3498 lappend r [list $t $gmtoff $isDst $abbrev]
3501 # In a version 2 file, there is also a POSIX-style time zone description
3502 # at the very end of the file. To get to it, skip over nLeap leap second
3503 # values (8 bytes each),
3504 # nIsStd standard/DST indicators and nIsGMT UTC/local indicators.
3506 if {$version eq {2}} {
3507 set seek [expr {$seek + 8 * $nLeap + $nIsStd + $nIsGMT + 1}]
3508 set last [string first \n $d $seek]
3509 set posix [string range $d $seek [expr {$last-1}]]
3510 if {[llength $posix] > 0} {
3511 set posixFields [ParsePosixTimeZone $posix]
3512 foreach tuple [ProcessPosixTimeZone $posixFields] {
3513 lassign $tuple t gmtoff isDst abbrev
3514 if {$t > $lastTime} {
3521 set TZData(:$fileName) $r
3526 #----------------------------------------------------------------------
3528 # ParsePosixTimeZone --
3530 # Parses the TZ environment variable in Posix form
3533 # tz Time zone specifier to be interpreted
3536 # Returns a dictionary whose values contain the various pieces of the
3537 # time zone specification.
3543 # Throws an error if the syntax of the time zone is incorrect.
3545 # The following keys are present in the dictionary:
3546 # stdName - Name of the time zone when Daylight Saving Time
3548 # stdSignum - Sign (+, -, or empty) of the offset from Greenwich
3549 # to the given (non-DST) time zone. + and the empty
3550 # string denote zones west of Greenwich, - denotes east
3551 # of Greenwich; this is contrary to the ISO convention
3552 # but follows Posix.
3553 # stdHours - Hours part of the offset from Greenwich to the given
3554 # (non-DST) time zone.
3555 # stdMinutes - Minutes part of the offset from Greenwich to the
3556 # given (non-DST) time zone. Empty denotes zero.
3557 # stdSeconds - Seconds part of the offset from Greenwich to the
3558 # given (non-DST) time zone. Empty denotes zero.
3559 # dstName - Name of the time zone when DST is in effect, or the
3560 # empty string if the time zone does not observe Daylight
3562 # dstSignum, dstHours, dstMinutes, dstSeconds -
3563 # Fields corresponding to stdSignum, stdHours, stdMinutes,
3564 # stdSeconds for the Daylight Saving Time version of the
3565 # time zone. If dstHours is empty, it is presumed to be 1.
3566 # startDayOfYear - The ordinal number of the day of the year on which
3567 # Daylight Saving Time begins. If this field is
3568 # empty, then DST begins on a given month-week-day,
3570 # startJ - The letter J, or an empty string. If a J is present in
3571 # this field, then startDayOfYear does not count February 29
3572 # even in leap years.
3573 # startMonth - The number of the month in which Daylight Saving Time
3574 # begins, supplied if startDayOfYear is empty. If both
3575 # startDayOfYear and startMonth are empty, then US rules
3577 # startWeekOfMonth - The number of the week in the month in which
3578 # Daylight Saving Time begins, in the range 1-5.
3579 # 5 denotes the last week of the month even in a
3581 # startDayOfWeek - The number of the day of the week (Sunday=0,
3582 # Saturday=6) on which Daylight Saving Time begins.
3583 # startHours - The hours part of the time of day at which Daylight
3584 # Saving Time begins. An empty string is presumed to be 2.
3585 # startMinutes - The minutes part of the time of day at which DST begins.
3586 # An empty string is presumed zero.
3587 # startSeconds - The seconds part of the time of day at which DST begins.
3588 # An empty string is presumed zero.
3589 # endDayOfYear, endJ, endMonth, endWeekOfMonth, endDayOfWeek,
3590 # endHours, endMinutes, endSeconds -
3591 # Specify the end of DST in the same way that the start* fields
3592 # specify the beginning of DST.
3594 # This procedure serves only to break the time specifier into fields. No
3595 # attempt is made to canonicalize the fields or supply default values.
3597 #----------------------------------------------------------------------
3599 proc ::tcl::clock::ParsePosixTimeZone { tz } {
3600 if {[regexp -expanded -nocase -- {
3602 # 1 - Standard time zone name
3603 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3604 # 2 - Standard time zone offset, signum
3606 # 3 - Standard time zone offset, hours
3609 # 4 - Standard time zone offset, minutes
3610 : ([[:digit:]]{1,2})
3612 # 5 - Standard time zone offset, seconds
3613 : ([[:digit:]]{1,2} )
3617 # 6 - DST time zone name
3618 ([[:alpha:]]+ | <[-+[:alnum:]]+>)
3621 # 7 - DST time zone offset, signum
3623 # 8 - DST time zone offset, hours
3626 # 9 - DST time zone offset, minutes
3627 : ([[:digit:]]{1,2})
3629 # 10 - DST time zone offset, seconds
3630 : ([[:digit:]]{1,2})
3637 # 11 - Optional J in n and Jn form 12 - Day of year
3638 ( J ? ) ( [[:digit:]]+ )
3640 # 13 - Month number 14 - Week of month 15 - Day of week
3642 [.] ( [[:digit:]] + )
3643 [.] ( [[:digit:]] + )
3646 # 16 - Start time of DST - hours
3647 / ( [[:digit:]]{1,2} )
3649 # 17 - Start time of DST - minutes
3650 : ( [[:digit:]]{1,2} )
3652 # 18 - Start time of DST - seconds
3653 : ( [[:digit:]]{1,2} )
3659 # 19 - Optional J in n and Jn form 20 - Day of year
3660 ( J ? ) ( [[:digit:]]+ )
3662 # 21 - Month number 22 - Week of month 23 - Day of week
3664 [.] ( [[:digit:]] + )
3665 [.] ( [[:digit:]] + )
3668 # 24 - End time of DST - hours
3669 / ( [[:digit:]]{1,2} )
3671 # 25 - End time of DST - minutes
3672 : ( [[:digit:]]{1,2} )
3674 # 26 - End time of DST - seconds
3675 : ( [[:digit:]]{1,2} )
3683 } $tz -> x(stdName) x(stdSignum) x(stdHours) x(stdMinutes) x(stdSeconds) \
3684 x(dstName) x(dstSignum) x(dstHours) x(dstMinutes) x(dstSeconds) \
3685 x(startJ) x(startDayOfYear) \
3686 x(startMonth) x(startWeekOfMonth) x(startDayOfWeek) \
3687 x(startHours) x(startMinutes) x(startSeconds) \
3688 x(endJ) x(endDayOfYear) \
3689 x(endMonth) x(endWeekOfMonth) x(endDayOfWeek) \
3690 x(endHours) x(endMinutes) x(endSeconds)] } {
3691 # it's a good timezone
3693 return [array get x]
3697 -errorcode [list CLOCK badTimeZone $tz] \
3698 "unable to parse time zone specification \"$tz\""
3701 #----------------------------------------------------------------------
3703 # ProcessPosixTimeZone --
3705 # Handle a Posix time zone after it's been broken out into fields.
3708 # z - Dictionary returned from 'ParsePosixTimeZone'
3711 # Returns time zone information for the 'TZData' array.
3716 #----------------------------------------------------------------------
3718 proc ::tcl::clock::ProcessPosixTimeZone { z } {
3722 # Determine the standard time zone name and seconds east of Greenwich
3724 set stdName [dict get $z stdName]
3725 if { [string index $stdName 0] eq {<} } {
3726 set stdName [string range $stdName 1 end-1]
3728 if { [dict get $z stdSignum] eq {-} } {
3733 set stdHours [lindex [::scan [dict get $z stdHours] %d] 0]
3734 if { [dict get $z stdMinutes] ne {} } {
3735 set stdMinutes [lindex [::scan [dict get $z stdMinutes] %d] 0]
3739 if { [dict get $z stdSeconds] ne {} } {
3740 set stdSeconds [lindex [::scan [dict get $z stdSeconds] %d] 0]
3744 set stdOffset [expr {
3745 (($stdHours * 60 + $stdMinutes) * 60 + $stdSeconds) * $stdSignum
3747 set data [list [list $MINWIDE $stdOffset 0 $stdName]]
3749 # If there's no daylight zone, we're done
3751 set dstName [dict get $z dstName]
3752 if { $dstName eq {} } {
3755 if { [string index $dstName 0] eq {<} } {
3756 set dstName [string range $dstName 1 end-1]
3759 # Determine the daylight name
3761 if { [dict get $z dstSignum] eq {-} } {
3766 if { [dict get $z dstHours] eq {} } {
3767 set dstOffset [expr { 3600 + $stdOffset }]
3769 set dstHours [lindex [::scan [dict get $z dstHours] %d] 0]
3770 if { [dict get $z dstMinutes] ne {} } {
3771 set dstMinutes [lindex [::scan [dict get $z dstMinutes] %d] 0]
3775 if { [dict get $z dstSeconds] ne {} } {
3776 set dstSeconds [lindex [::scan [dict get $z dstSeconds] %d] 0]
3780 set dstOffset [expr {
3781 (($dstHours*60 + $dstMinutes) * 60 + $dstSeconds) * $dstSignum
3785 # Fill in defaults for European or US DST rules
3786 # US start time is the second Sunday in March
3787 # EU start time is the last Sunday in March
3788 # US end time is the first Sunday in November.
3789 # EU end time is the last Sunday in October
3792 [dict get $z startDayOfYear] eq {}
3793 && [dict get $z startMonth] eq {}
3795 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3797 dict set z startWeekOfMonth 5
3799 dict set z startHours 2
3801 dict set z startHours [expr {$stdHours+1}]
3805 dict set z startWeekOfMonth 2
3806 dict set z startHours 2
3808 dict set z startMonth 3
3809 dict set z startDayOfWeek 0
3810 dict set z startMinutes 0
3811 dict set z startSeconds 0
3814 [dict get $z endDayOfYear] eq {}
3815 && [dict get $z endMonth] eq {}
3817 if {($stdSignum * $stdHours>=0) && ($stdSignum * $stdHours<=12)} {
3819 dict set z endMonth 10
3820 dict set z endWeekOfMonth 5
3822 dict set z endHours 3
3824 dict set z endHours [expr {$stdHours+2}]
3828 dict set z endMonth 11
3829 dict set z endWeekOfMonth 1
3830 dict set z endHours 2
3832 dict set z endDayOfWeek 0
3833 dict set z endMinutes 0
3834 dict set z endSeconds 0
3837 # Put DST in effect in all years from 1916 to 2099.
3839 for { set y 1916 } { $y < 2100 } { incr y } {
3840 set startTime [DeterminePosixDSTTime $z start $y]
3841 incr startTime [expr { - wide($stdOffset) }]
3842 set endTime [DeterminePosixDSTTime $z end $y]
3843 incr endTime [expr { - wide($dstOffset) }]
3844 if { $startTime < $endTime } {
3846 [list $startTime $dstOffset 1 $dstName] \
3847 [list $endTime $stdOffset 0 $stdName]
3850 [list $endTime $stdOffset 0 $stdName] \
3851 [list $startTime $dstOffset 1 $dstName]
3858 #----------------------------------------------------------------------
3860 # DeterminePosixDSTTime --
3862 # Determines the time that Daylight Saving Time starts or ends from a
3863 # Posix time zone specification.
3866 # z - Time zone data returned from ParsePosixTimeZone.
3867 # Missing fields are expected to be filled in with
3869 # bound - The word 'start' or 'end'
3870 # y - The year for which the transition time is to be determined.
3873 # Returns the transition time as a count of seconds from the epoch. The
3874 # time is relative to the wall clock, not UTC.
3876 #----------------------------------------------------------------------
3878 proc ::tcl::clock::DeterminePosixDSTTime { z bound y } {
3882 # Determine the start or end day of DST
3884 set date [dict create era CE year $y]
3885 set doy [dict get $z ${bound}DayOfYear]
3888 # Time was specified as a day of the year
3890 if { [dict get $z ${bound}J] ne {}
3891 && [IsGregorianLeapYear $y]
3892 && ( $doy > $FEB_28 ) } {
3895 dict set date dayOfYear $doy
3896 set date [GetJulianDayFromEraYearDay $date[set date {}] 2361222]
3898 # Time was specified as a day of the week within a month
3900 dict set date month [dict get $z ${bound}Month]
3901 dict set date dayOfWeek [dict get $z ${bound}DayOfWeek]
3902 set dowim [dict get $z ${bound}WeekOfMonth]
3903 if { $dowim >= 5 } {
3906 dict set date dayOfWeekInMonth $dowim
3907 set date [GetJulianDayFromEraYearMonthWeekDay $date[set date {}] 2361222]
3911 set jd [dict get $date julianDay]
3913 wide($jd) * wide(86400) - wide(210866803200)
3916 set h [dict get $z ${bound}Hours]
3920 set h [lindex [::scan $h %d] 0]
3922 set m [dict get $z ${bound}Minutes]
3926 set m [lindex [::scan $m %d] 0]
3928 set s [dict get $z ${bound}Seconds]
3932 set s [lindex [::scan $s %d] 0]
3934 set tod [expr { ( $h * 60 + $m ) * 60 + $s }]
3935 return [expr { $seconds + $tod }]
3938 #----------------------------------------------------------------------
3942 # Given local time expressed in seconds from the Posix epoch,
3943 # determine localized era and year within the era.
3946 # date - Dictionary that must contain the keys, 'localSeconds',
3947 # whose value is expressed as the appropriate local time;
3948 # and 'year', whose value is the Gregorian year.
3949 # etable - Value of the LOCALE_ERAS key in the message catalogue
3950 # for the target locale.
3953 # Returns the dictionary, augmented with the keys, 'localeEra' and
3956 #----------------------------------------------------------------------
3958 proc ::tcl::clock::GetLocaleEra { date etable } {
3959 set index [BSearch $etable [dict get $date localSeconds]]
3961 dict set date localeEra \
3962 [::format %02d [expr { [dict get $date year] / 100 }]]
3963 dict set date localeYear [expr {
3964 [dict get $date year] % 100
3967 dict set date localeEra [lindex $etable $index 1]
3968 dict set date localeYear [expr {
3969 [dict get $date year] - [lindex $etable $index 2]
3975 #----------------------------------------------------------------------
3977 # GetJulianDayFromEraYearDay --
3979 # Given a year, month and day on the Gregorian calendar, determines
3980 # the Julian Day Number beginning at noon on that date.
3983 # date -- A dictionary in which the 'era', 'year', and
3984 # 'dayOfYear' slots are populated. The calendar in use
3985 # is determined by the date itself relative to:
3986 # changeover -- Julian day on which the Gregorian calendar was
3987 # adopted in the current locale.
3990 # Returns the given dictionary augmented with a 'julianDay' key whose
3991 # value is the desired Julian Day Number, and a 'gregorian' key that
3992 # specifies whether the calendar is Gregorian (1) or Julian (0).
3998 # This code needs to be moved to the C layer.
4000 #----------------------------------------------------------------------
4002 proc ::tcl::clock::GetJulianDayFromEraYearDay {date changeover} {
4003 # Get absolute year number from the civil year
4005 switch -exact -- [dict get $date era] {
4007 set year [expr { 1 - [dict get $date year] }]
4010 set year [dict get $date year]
4013 set ym1 [expr { $year - 1 }]
4015 # Try the Gregorian calendar first.
4017 dict set date gregorian 1
4020 + [dict get $date dayOfYear]
4027 # If the date is before the Gregorian change, use the Julian calendar.
4029 if { $jd < $changeover } {
4030 dict set date gregorian 0
4033 + [dict get $date dayOfYear]
4039 dict set date julianDay $jd
4043 #----------------------------------------------------------------------
4045 # GetJulianDayFromEraYearMonthWeekDay --
4047 # Determines the Julian Day number corresponding to the nth given
4048 # day-of-the-week in a given month.
4051 # date - Dictionary containing the keys, 'era', 'year', 'month'
4052 # 'weekOfMonth', 'dayOfWeek', and 'dayOfWeekInMonth'.
4053 # changeover - Julian Day of adoption of the Gregorian calendar
4056 # Returns the given dictionary, augmented with a 'julianDay' key.
4062 # This code needs to be moved to the C layer.
4064 #----------------------------------------------------------------------
4066 proc ::tcl::clock::GetJulianDayFromEraYearMonthWeekDay {date changeover} {
4067 # Come up with a reference day; either the zeroeth day of the given month
4068 # (dayOfWeekInMonth >= 0) or the seventh day of the following month
4069 # (dayOfWeekInMonth < 0)
4072 set week [dict get $date dayOfWeekInMonth]
4074 dict set date2 dayOfMonth 0
4076 dict incr date2 month
4077 dict set date2 dayOfMonth 7
4079 set date2 [GetJulianDayFromEraYearMonthDay $date2[set date2 {}] \
4081 set wd0 [WeekdayOnOrBefore [dict get $date dayOfWeek] \
4082 [dict get $date2 julianDay]]
4083 dict set date julianDay [expr { $wd0 + 7 * $week }]
4087 #----------------------------------------------------------------------
4089 # IsGregorianLeapYear --
4091 # Determines whether a given date represents a leap year in the
4092 # Gregorian calendar.
4095 # date -- The date to test. The fields, 'era', 'year' and 'gregorian'
4099 # Returns 1 if the year is a leap year, 0 otherwise.
4104 #----------------------------------------------------------------------
4106 proc ::tcl::clock::IsGregorianLeapYear { date } {
4107 switch -exact -- [dict get $date era] {
4109 set year [expr { 1 - [dict get $date year]}]
4112 set year [dict get $date year]
4115 if { $year % 4 != 0 } {
4117 } elseif { ![dict get $date gregorian] } {
4119 } elseif { $year % 400 == 0 } {
4121 } elseif { $year % 100 == 0 } {
4128 #----------------------------------------------------------------------
4130 # WeekdayOnOrBefore --
4132 # Determine the nearest day of week (given by the 'weekday' parameter,
4133 # Sunday==0) on or before a given Julian Day.
4136 # weekday -- Day of the week
4137 # j -- Julian Day number
4140 # Returns the Julian Day Number of the desired date.
4145 #----------------------------------------------------------------------
4147 proc ::tcl::clock::WeekdayOnOrBefore { weekday j } {
4148 set k [expr { ( $weekday + 6 ) % 7 }]
4149 return [expr { $j - ( $j - $k ) % 7 }]
4152 #----------------------------------------------------------------------
4156 # Service procedure that does binary search in several places inside the
4160 # list - List of lists, sorted in ascending order by the
4162 # key - Value to search for
4165 # Returns the index of the greatest element in $list that is less than
4171 #----------------------------------------------------------------------
4173 proc ::tcl::clock::BSearch { list key } {
4174 if {[llength $list] == 0} {
4177 if { $key < [lindex $list 0 0] } {
4182 set u [expr { [llength $list] - 1 }]
4185 # At this point, we know that
4186 # $k >= [lindex $list $l 0]
4187 # Either $u == [llength $list] or else $k < [lindex $list $u+1 0]
4188 # We find the midpoint of the interval {l,u} rounded UP, compare
4189 # against it, and set l or u to maintain the invariant. Note that the
4190 # interval shrinks at each step, guaranteeing convergence.
4192 set m [expr { ( $l + $u + 1 ) / 2 }]
4193 if { $key >= [lindex $list $m 0] } {
4196 set u [expr { $m - 1 }]
4203 #----------------------------------------------------------------------
4207 # Adds an offset to a given time.
4210 # clock add clockval ?count unit?... ?-option value?
4213 # clockval -- Starting time value
4214 # count -- Amount of a unit of time to add
4215 # unit -- Unit of time to add, must be one of:
4216 # years year months month weeks week
4217 # days day hours hour minutes minute
4222 # (Deprecated) Flag synonymous with '-timezone :GMT'
4224 # Name of the time zone in which calculations are to be done.
4226 # Name of the locale in which calculations are to be done.
4227 # Used to determine the Gregorian change date.
4230 # Returns the given time adjusted by the given offset(s) in
4234 # It is possible that adding a number of months or years will adjust the
4235 # day of the month as well. For instance, the time at one month after
4236 # 31 January is either 28 or 29 February, because February has fewer
4239 #----------------------------------------------------------------------
4241 proc ::tcl::clock::add { clockval args } {
4242 if { [llength $args] % 2 != 0 } {
4243 set cmdName "clock add"
4244 return -code error \
4245 -errorcode [list CLOCK wrongNumArgs] \
4246 "wrong \# args: should be\
4247 \"$cmdName clockval ?number units?...\
4248 ?-gmt boolean? ?-locale LOCALE? ?-timezone ZONE?\""
4250 if { [catch { expr {wide($clockval)} } result] } {
4251 return -code error $result
4257 set timezone [GetSystemTimeZone]
4259 foreach { a b } $args {
4260 if { [string is integer -strict $a] } {
4261 lappend offsets $a $b
4263 switch -exact -- $a {
4267 -l - -lo - -loc - -loca - -local - -locale {
4268 set locale [string tolower $b]
4270 -t - -ti - -tim - -time - -timez - -timezo - -timezon -
4275 throw [list CLOCK badOption $a] \
4276 "bad option \"$a\",\
4277 must be -gmt, -locale or -timezone"
4283 # Check options for validity
4285 if { [info exists saw(-gmt)] && [info exists saw(-timezone)] } {
4286 return -code error \
4287 -errorcode [list CLOCK gmtWithTimezone] \
4288 "cannot use -gmt and -timezone in same call"
4290 if { [catch { expr { wide($clockval) } } result] } {
4291 return -code error "expected integer but got \"$clockval\""
4293 if { ![string is boolean -strict $gmt] } {
4294 return -code error "expected boolean value but got \"$gmt\""
4301 set changeover [mc GREGORIAN_CHANGE_DATE]
4303 if {[catch {SetupTimeZone $timezone} retval opts]} {
4304 dict unset opts -errorinfo
4305 return -options $opts $retval
4309 foreach { quantity unit } $offsets {
4310 switch -exact -- $unit {
4312 set clockval [AddMonths [expr { 12 * $quantity }] \
4313 $clockval $timezone $changeover]
4316 set clockval [AddMonths $quantity $clockval $timezone \
4321 set clockval [AddDays [expr { 7 * $quantity }] \
4322 $clockval $timezone $changeover]
4325 set clockval [AddDays $quantity $clockval $timezone \
4330 set clockval [expr { 3600 * $quantity + $clockval }]
4333 set clockval [expr { 60 * $quantity + $clockval }]
4336 set clockval [expr { $quantity + $clockval }]
4340 throw [list CLOCK badUnit $unit] \
4341 "unknown unit \"$unit\", must be \
4342 years, months, weeks, days, hours, minutes or seconds"
4347 } trap CLOCK {result opts} {
4348 # Conceal the innards of [clock] when it's an expected error
4349 dict unset opts -errorinfo
4350 return -options $opts $result
4354 #----------------------------------------------------------------------
4358 # Add a given number of months to a given clock value in a given
4362 # months - Number of months to add (may be negative)
4363 # clockval - Seconds since the epoch before the operation
4364 # timezone - Time zone in which the operation is to be performed
4367 # Returns the new clock value as a number of seconds since
4373 #----------------------------------------------------------------------
4375 proc ::tcl::clock::AddMonths { months clockval timezone changeover } {
4376 variable DaysInRomanMonthInCommonYear
4377 variable DaysInRomanMonthInLeapYear
4380 # Convert the time to year, month, day, and fraction of day.
4382 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4383 dict set date secondOfDay [expr {
4384 [dict get $date localSeconds] % 86400
4386 dict set date tzName $timezone
4388 # Add the requisite number of months
4390 set m [dict get $date month]
4393 set delta [expr { $m / 12 }]
4394 set mm [expr { $m % 12 }]
4395 dict set date month [expr { $mm + 1 }]
4396 dict incr date year $delta
4398 # If the date doesn't exist in the current month, repair it
4400 if { [IsGregorianLeapYear $date] } {
4401 set hath [lindex $DaysInRomanMonthInLeapYear $mm]
4403 set hath [lindex $DaysInRomanMonthInCommonYear $mm]
4405 if { [dict get $date dayOfMonth] > $hath } {
4406 dict set date dayOfMonth $hath
4409 # Reconvert to a number of seconds
4411 set date [GetJulianDayFromEraYearMonthDay \
4414 dict set date localSeconds [expr {
4416 + ( 86400 * wide([dict get $date julianDay]) )
4417 + [dict get $date secondOfDay]
4419 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4422 return [dict get $date seconds]
4426 #----------------------------------------------------------------------
4430 # Add a given number of days to a given clock value in a given time
4434 # days - Number of days to add (may be negative)
4435 # clockval - Seconds since the epoch before the operation
4436 # timezone - Time zone in which the operation is to be performed
4437 # changeover - Julian Day on which the Gregorian calendar was adopted
4438 # in the target locale.
4441 # Returns the new clock value as a number of seconds since the epoch.
4446 #----------------------------------------------------------------------
4448 proc ::tcl::clock::AddDays { days clockval timezone changeover } {
4451 # Convert the time to Julian Day
4453 set date [GetDateFields $clockval $TZData($timezone) $changeover]
4454 dict set date secondOfDay [expr {
4455 [dict get $date localSeconds] % 86400
4457 dict set date tzName $timezone
4459 # Add the requisite number of days
4461 dict incr date julianDay $days
4463 # Reconvert to a number of seconds
4465 dict set date localSeconds [expr {
4467 + ( 86400 * wide([dict get $date julianDay]) )
4468 + [dict get $date secondOfDay]
4470 set date [ConvertLocalToUTC $date[set date {}] $TZData($timezone) \
4473 return [dict get $date seconds]
4477 #----------------------------------------------------------------------
4479 # ChangeCurrentLocale --
4481 # The global locale was changed within msgcat.
4482 # Clears the buffered parse functions of the current locale.
4491 # Buffered parse functions are cleared.
4493 #----------------------------------------------------------------------
4495 proc ::tcl::clock::ChangeCurrentLocale {args} {
4497 variable LocaleNumeralCache
4498 variable CachedSystemTimeZone
4499 variable TimeZoneBad
4501 foreach p [info procs [namespace current]::scanproc'*'current] {
4504 foreach p [info procs [namespace current]::formatproc'*'current] {
4508 catch {array unset FormatProc *'current}
4509 set LocaleNumeralCache {}
4512 #----------------------------------------------------------------------
4516 # Clears all caches to reclaim the memory used in [clock]
4525 # Caches are cleared.
4527 #----------------------------------------------------------------------
4529 proc ::tcl::clock::ClearCaches {} {
4531 variable LocaleNumeralCache
4532 variable CachedSystemTimeZone
4533 variable TimeZoneBad
4535 foreach p [info procs [namespace current]::scanproc'*] {
4538 foreach p [info procs [namespace current]::formatproc'*] {
4542 catch {unset FormatProc}
4543 set LocaleNumeralCache {}
4544 catch {unset CachedSystemTimeZone}