Date and Time Issues

Description

questions, answers, and discussion of the clock function.

Discussion


A common question is How do I format the current time into some specific format (such as hh:mm:ss)?

Try

clock format [clock seconds] -format %T

or

clock format [clock seconds] -format %X

or if you need something more formatted, try

clock format [clock seconds] -format {%H hours %M minutes and %S seconds}

MPJ (2004/11/22) Question: The man pages for clock scan command implies that you can read a ISO 8601 point-in-time specifications. The reason for this question while parsing some RSS feeds the date field is set in 8601 format and Tcl did not like this data. So when I looked at the specification and examples from [L1 ] I find that not all case are able to be parsed (Tcl 8.4.7). Therefore my question is how would you scan this data in correctly and is this command broken for ISO 8601? See test the results below:

Year:

# YYYY (eg 1997)
% clock format [clock scan 1997]
 unable to convert date-time string "1997"

Year and month:

# YYYY-MM (eg 1997-07)
% clock format [clock scan 1997-07]
 unable to convert date-time string "1997-07"

Complete date:

# YYYY-MM-DD (eg 1997-07-16)
% clock format [clock scan 1997-07-16] 
 Wed Jul 16 12:00:00 AM Eastern Daylight Time 1997

Complete date plus hours and minutes:

# YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
% clock format [clock scan 1997-07-16T19:20+01:00]
 unable to convert date-time string "1997-07-16T19:20+01:00"

# but if we remove the time zone offset it works but is one hour to fast!!!
% clock format [clock scan 1997-07-16T19:20]
 Wed Jul 16 8:20:00 AM Eastern Daylight Time 1997

# removing the T gives us the correct time ?? strange
% clock format [clock scan "1997-07-16 19:20"]
 Wed Jul 16 7:20:00 PM Eastern Daylight Time 1997

# also if we remove the dashes and leave the T 
% clock format [clock scan "19970716T19:20:00"]
 Wed Jul 16 7:20:00 PM Eastern Daylight Time 1997

Complete date plus hours, minutes and seconds:

# YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
% clock format [clock scan 1997-07-16T19:20:30+01:00]
 unable to convert date-time string "1997-07-16T19:20:30+01:00"

Complete date plus hours, minutes, seconds and a decimal fraction of a second

# YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
% clock format [clock scan 1997-07-16T19:20:30.45+01:00]
 unable to convert date-time string "1997-07-16T19:20:30.45+01:00"

where:

YYYY = four-digit year
MM   = two-digit month (01=January, etc.)
DD   = two-digit day of month (01 through 31)
hh   = two digits of hour (00 through 23) (am/pm NOT allowed)
mm   = two digits of minute (00 through 59)
ss   = two digits of second (00 through 59)
s    = one or more digits representing a decimal fraction of a second
TZD  = time zone designator (Z or +hh:mm or -hh:mm)

Question: Does anyone have a pointer to a Tcl/Tk countdown clock - given a particular date/time, the number of years/months/weeks/days/hours/minutes/seconds until it is reached ticks away? Would be used for deadlines, retirement days, etc.

See Years, months, days, etc. between two dates for a partial solution.

MNO I knocked up A Little Countdown Clock for a friend who was looking forward to finishing his contract. It counts down in fractions of a second (mainly to catch the eye ;), and displays in seconds.


Here's a common question that I have answered more than once: "How do I calculate elapsed time?"


First, set your "granularity". Here I go for hours.

% set secondsPerHour 3600.00
3600.00

Then, use something like this:

% set elapsedTime [expr {([clock scan "2:15 PM"] - [clock scan "11:00 AM"]) / $secondsPerHour}]
3.25

I'm sure there are other questions concerning the exceedingly complex and useful clock function.....

DKF - I corrected a minor formatting problem...


How does one easily calculate changes in time over time zones? I.e. How do I find out how many hours have elapsed between 1:00 AM CDT and 1:00 AM GMT?

DKF - Use [clock scan] to convert them both into standard format (i.e. seconds from beginning of epoch.) Then it is trivial to work out the time difference between them...

% set cdt [clock scan "1:00 AM CDT"]
969343200
% set gmt [clock scan "1:00 AM GMT"]
969325200
% expr ($cdt-$gmt)/3600.0; # in hours...
5.0

LV - One pretty common problem/question with dates and times occurs when someone gets a date or time in a format in which the value has a leading zero, then attempts to use that. For 8 and 9 am (and pm) as well as for the 8th and 9th of the month, one has a 08 or 09 as a value, resulting in Tcl generating the error msg:

  "08" is an invalid octal number
 while evaluating {set b [expr $a + 1]}

or whatever.

DKF - The correct way to deal with this is to use [scan %d] to convert the digit string into a number:

scan $a %d aNum
set b [expr {$a+1}]

LV - another question I've seen people ask is about getting the output they produce relating to dates and times to show a leading 0, since Tcl's treatment of numbers generally drops leading zeros for the reason just mentioned (octal number treatment).

DKF - And the natural complement of [scan] is [format] which can do precise this with minimal fuss:

 puts [format %04d 123]

RS - ... and [clock format] can do the typical application, zero-padded two-digit hours, minutes, seconds etc, with even less fuss:

 clock format [clock seconds] -format %y%m%d-%H%M%S
 001004-091226

LV - another question I see a lot - how to take a user provided date and compare it to the current date, or to calculate a future/past date to it.

KBK -

# Compare two dates

set date1 [clock scan {03/09/2001}]
set date2 [clock scan {02/08/2001}]
if { [expr { $date1 < $date2 }] } {
    puts "$date1 is earlier"
}

# Calculate a date relative to another date

% set t [clock scan {03/09/2001}]
984114000
% set u [clock scan {+3 days} -base $t]
984373200
% clock format $u
Mon Mar 12 00:00:00 Eastern Standard Time 2001
% set u [clock scan {+3 weeks} -base $t]
985928400
% clock format $u
Fri Mar 30 00:00:00 Eastern Standard Time 2001
% set u [clock scan {+3 months} -base $t]
992059200
% clock format $u
Sat Jun 09 00:00:00 Eastern Daylight Time 2001

See also Date Calculations.


Dave Griffin - For server-based applications, you may want to format the time for a timezone other than the local server's time. This seems to do the trick well enough:

#
# Format a date, optionally adjusting the local timezone
#    date    - Tcl clock value
#    fmt     - Date formatting string
#    gmt     - True: format for GMT; False: local time
#    localTz - Local timezone setting (e.g., EST5EDT)
#              (if empty, use server local timezone)
#

proc formatDateTz { date fmt gmt localTz} {
    global env

    set saveTz ""

    if {$localTz != ""} {
        catch { set saveTz $env(TZ) }
        set env(TZ) $localTz
    }

    set r [clock format $date -format $fmt -gmt $gmt]

    if {$localTz != ""} {
        if {$saveTz != ""} {
            set env(TZ) $saveTz
        } else {
            unset env(TZ)
        }
    }
    
    return $r
}

Please note that I believe the setting of localTz (the env(TZ) variable) is system-dependent. If I'm wrong here, I'd love to know the syntax -- there seem to be several to choose from.

RHS 10Sept2004 Will this cause problems in a multithreaded tcl? I believe env is shared across threads, so the lack of a lock around the code means that you could wind up with issues.


LV New Issue: how to handle dates farther in the future than 2038. Is there a plan for Tcl to handle these?

 $ clock scan {12/31/2039}

unable to convert date-time string "12/31/2039" while evaluating {clock scan {12/31/2039}}

Check out

What: clock with extended year range
Where: http://members.home.net/arthur.taylor/graph/clock2.tar.gz  
Description: Extension adding halo_clock2 command, which has most if not
       all of Tcl's clock functionality, but using a double int instead of
       a long int .  This extends the years covered to BC.  Also adds
       a IsDaylightSavings feature.
Updated: 03/2001
Contact: mailto:[email protected]  (Arthur Taylor)

KBK 8.5 does not have the Y2038 problem.

'''Number of days in a given month''': from [A little date chooser]
proc numberofdays {month year} {
    if {$month==12} {set month 0; incr year}
    clock format [clock scan "[incr month]/1/$year  1 day ago"] \
        -format %d
} ;# RS

What: Critchlow's Tcl support routines
Where: http://www.elf.org/tclsources.html  
Description: Mr. Critchlow provides a variety of useful tcl routines at
        his site.  For instance, palette is a reworking of Eric Grosse's
        rainbow color palette generator from netlib.
        This allows you to generate a variety of color palettes
        in Tcl.
        There's also a collection of time and date computations written in Tcl,
        which solve the problem of converting Unix clock tics into
        calendar date along with several other date/time needs.
        An implementation of George Marsaglia's mother of all random number
        generators, written in Tcl is alson on the site.  This is a
        very long period pseudorandom number generator.
        Also a version of old-random.tcl without namespaces.
        A Tcl procedure for converting an XPM into an photo widget image.
Updated: 04/1999
Contact: mailto:[email protected]   (Roger E. Critchlow Jr.)

The above code deals with standard Tcl date range, but provides Julian to calendar date conversions (and vice versa), as well as day and time to date with fraction and vice versa conversions.


Dave Griffin - the clock format command has a %Z option which returns the zone name, but no easy way to get the current timezone offset. (The zone name can be horribly non-standard on Windows systems: "Eastern Standard Time" instead of EST). RFC2822 also demands that dates use numeric timezone offsets. The following returns a RFC2822 compatible timezone offset:

proc RFC2822TimezoneOffset {} {
    set gmt [clock seconds]
    set local1 [clock format [clock seconds] -format "%Y%m%d %H:%M:%S"]
    set local [clock scan $local1 -gmt 1]
    set offset [expr $local - $gmt]
    if {$offset < 0} {
        return "-[clock format [expr abs($offset)] -format "%H%M" -gmt 1]"
    } else {
        return "+[clock format $offset -format "%H%M" -gmt 1]"
    }
}

KBK - In Tcl 8.5, you have both %Z and %z (numeric or named time zones), and Tcl tries hard to avoid using Windows timezone names.


Timezone offset, local vs. UTC, in hours:

expr {([clock scan 0 -gmt 1]-[clock scan 0])/3600} ;# RS

rpremuz (2008-12-05) This actually means the following:

 expr {([clock scan "today 00:00" -gmt 1]-[clock scan "today 00:00"])/3600}

and hence it gives the expected result only if the current time in the local time zone and UTC belong to the same day. Otherwise the offset is increased or decreased for 24 hours. Moreover, the time interval should be divided by 3600.0 because the offset need not be an integer. Another problem is that there are time zones which have offset larger than 12 hours. See [L2 ].

The RFC2822TimezoneOffset proc above works well.


Donal Fellows wrote in comp.lang.tcl: To get the number of days in the *current* month:

set startOfMonth [clock format [clock seconds] -format %m/01/%Y]
set days [clock format [clock scan "+1 month -1 day" \
        -base [clock scan $startOfMonth] -format %d]

yahalom emet tcl version: tcl8.4.1 when I do:

set month [clock format [clock second] -format %b]

I am getting the month name in a local format why? How can I get it in english?

rmax: Starting with version 8.4 [clock format] respects locale settings on Unix. So depending of the contents of the LANG, LC_TIME, or LC_ALL environment variables you get localized day and month names. If you always want to have english names, you could

set env(LC_TIME) POSIX

inside your application.

KBK - And starting with 8.5, you get localised time only if you've expressly requested it with a '-locale' setting (-locale system for the system locale; -locale current for whatever mclocale is currently set to). Defaulting to the C locale is intentional - far more times are formatted to be read by programs than by humans.


escargo 22 Nov 2002 - According to http://tycho.usno.navy.mil/leapsec.html the current date has had at least 22 leap seconds inserted since 1972, which I believe is after the starting basis for clock time keeping [L3 ].

(clock format 0 gives Wed Dec 31 18:00:00 Central Standard Time 1969.)

At the very least, the documentation clock scan and clock format should mention that there can be some inaccuracy in translating between seconds and dates.

KBK There is no inaccuracy. [clock seconds] gives seconds of nominal time; it assumes precisely 86,400 seconds per day. Leap seconds are handled by smoothing the clock according to the ideas in http://www.cl.cam.ac.uk/~mgk25/uts.txt . This behavior is practically mandated for Posix compliance; while Linux (and some other systems using the Olson codes) can be configured to behave differently, it's no longer Posix if you do.

escargo 22 Nov 2002 - Perfectly understandable; everybody should be wrong the standard way. I just find it annoying that I have a point in time slightly after midnight, and then ask for a date some number of seconds in the past, I might get the wrong answer. It might be the standard and mandated answer, but it would still be wrong.

KBK 25 Nov 2002 - I've actually spent a fair amount of time thinking about this issue. While it's initially attractive to have the 'seconds' value represent absolute time (the independent variable in Newton's Laws of Motion), it's also tremendously inconvenient in practice, except for the small minority of applications that deal with controlling things in the physical world (and even most of them can deal with smoothed time, if they know to expect it). The key issue is that many applications want to represent times months or years in the future -- before the leap-second times are published. Nevertheless, they'd rather deal with time as if it marches linearly, 86,400 seconds per day, so that everyone can agree that [clock format 1141862400] will represent midnight, UTC, on 9 March 2006, without worrying about whether three or four leap seconds will be inserted between now and then - and as I write this, nobody knows what the number of leap seconds will be; it depends on astronomical observations that haven't been made yet.

The smoothed scheme gives (assuming that a system has a stable external source for timing):

  • a uniform model of time for date and time arithmetic; 86,400 seconds per day with no corrections.
  • times measurable to microsecond precision.
  • interval times precise to one part per thousand (worst case) and typically two orders of magnitude better (when the clock isn't sliding)
  • absolute times precise to about a second (worst case) and typically three or four orders of magnitude better.

These numbers, admittedly, aren't good enough for applications like clock synchronization in CDMA systems or radar tracking of spacecraft. On the other hand, they free the typical application from needing to be aware of the details of timekeeping, which can otherwise get ugly FAST.

What is the right answer to "the time 86,400 seconds in the past?" For a very few applications, it's "this time yesterday, plus or minus a leap second that may have intervened." For the vast majority, it's simply an easy shorthand for "this time yesterday, and don't trouble me with the details!" Having [clock seconds] represent nominal time on a civil clock keeps the simple things simple.

escargo 25 Nov 2002 - Again, I understand and sympathize, but what if the question is, "What was the date 864,000 seconds in the past?" The required precision is to the day, not to the second. Would it not be nice to get the right answer? I realize that some of this is a precision versus accuracy issue, but, as I indicated, at least the problem should be documented.

Maybe you cannot forecast leap seconds into the future, but hypothetically at least you could deal with them for past dates.


Here's an algorithm for determining the day of the week from an ansi date:

set date 2004-01-09
regexp {([0-9]*)-([0-9]*)-([0-9]*)} $date match year month day
regexp {0(.)} $month match month extra
regexp {0(.)} $day match day extra
set alpha [expr {(14 - $month) / 12}]
set y [expr {$year - $alpha}]
set m [expr {$month + (12 * $alpha) - 2}]
set day_of_week_pre_mod [expr {$day + $y + ($y / 4) - ($y / 100) + ($y / 400) + (31 * $m / 12)}]
set day_of_week [expr {$day_of_week_pre_mod % 7}]

0 is a Sunday

RS: Note that Tcl has that functionality built in:

 % clock format [clock scan $date] -format %w
 5

Writing AM, PM, Am, Pm, am, pm

RA2 A quick question. I have a date/time stamper. The Am, Pm is written in capital letters (AM, PM). Is there a way to put the AM/PM in lowercase (am/pm) or better with the a and the p capitalized but not the M (Am/Pm)? Thanks!

Salut, Robert. You can use [string map] for this job, but note that most people consider Am/Pm to be wrong.

 string map [list AM Am PM Pm] $string

Un grand merci, 202. Michel? I'll try this right away!

KBK In 8.5, you could also define a locale that has whatever strings you please.


See also Reworking the clock command.


For stardate formatting (input and output) see Star Trek.


MNO Quick and dirty time interval calculation into weeks, days, hours, minutes, seconds etc. and although I did have a quick look, it won't surprise me in the least when someone points out that this functionality already exists in tcllib (or [clock]!).

set end [clock scan "28 March 2005 0:00"]
set now [clock seconds]
set int [expr $end - $now]

if { $int <= 0 } {
    puts "End date is not in the future (or integer overflow occurred!?)"
    exit
}

set out "seconds"
set intervals [list 60 \
        minutes 60 \
        hours 24 \
        days 7 \
        weeks 52 \
        years 10 \
        decades 10 \
        centuries 10 \
        millennia]

foreach { mult name } $intervals {
    set rem [expr $int % $mult]
    set int [expr $int / $mult]
    set out "$rem $out"
    if { $int == 0 } { break }
    set out "$name, $out"
}

puts $out

davidw at a client's, but wanted to take a moment to include an improved version of the above that could go into tcllib.

 package provide timeleft 0.1

 package require msgcat

 namespace eval timeleft {
     namespace import ::msgcat::mc

     array set singulars {
         seconds second
         minutes minute
         hours hour
         days day
         weeks week
         years year
         decades decade
         centuries century
         millennia millennium
     }
 }

 proc timeleft::timeleft {int} {
     variable singulars
     if { $int <= 0 } {
         error "End date is not in the future (or integer overflow occurred!?)"
     }

     set out "seconds"
     set intervals [list 60 \
                        minutes 60 \
                        hours 24 \
                        days 7 \
                        weeks 52 \
                        years 10 \
                        decades 10 \
                        centuries 10 \
                        millennia]

     foreach { mult name } $intervals {
         set rem [expr $int % $mult]
         set int [expr $int / $mult]
         set out "$rem $out"
         if { $int == 0 } { break }
         set out "$name $out"
     }
     set res ""

     foreach {num unit} $out {
         if {$num == 1} {
             set unitname $singulars($unit)
         } else {
             set unitname $unit
         }
         lappend res "$num [mc $unitname]"
     }

     return [join $res ", "]
 }

RS 2007-02-08 ...or maybe this is enough? It uses weeks as biggest unit, since a year never has 7*52=364 days :)

proc timediff reltime {
    set w [expr {$reltime/86400/7}]
    set d [expr {$reltime/86400%7}]
    list $w week(s) $d day(s) \
        [clock format $reltime -gmt 1 -format %H:%M:%S]
}
 % timediff 86500
 0 week(s) 1 day(s) 00:01:40
 % timediff 8650000
 14 week(s) 2 day(s) 02:46:40
 % timediff 86500000
 143 week(s) 0 day(s) 03:46:40
 % timediff 86500001
 143 week(s) 0 day(s) 03:46:41

KBK If you don't care about the intricacies of the calendar and clock, the proposed code will do nicely. But there are ways of calculating the years, months, days, etc. between two dates that *do* take the complexities into account.


LES 2009-08-08: Meh. I should have searched Wikit before trying to make my own. Now it's too late, I've made it. But I think my code has two advantages: easier to customize (add or remove units) and a teensy bit more elegant output. The disadvantage is that it completely disregards the fact that not all months have 30 days.

proc p.spell.seconds { argseconds } {
    set steps " 
        zero
        year [expr {86400 * 365}]
        month [expr {86400 * 30}]
        week 604800
        day 86400
        hour 3600
        minute 60
        second 1
    "

    set i 1
    set _templist {zero}
    while {$i < [llength $steps]} {
        set unit [lindex $steps $i]
        incr i
        set number [lindex $steps $i]
        incr i
        set argseconds [::tcl::mathfunc::int $argseconds]
        if {$number > $argseconds} {continue}
        set nunit [expr {$argseconds / $number}]
        set argseconds [::tcl::mathfunc::fmod $argseconds $number]
        if {$nunit > 1} {set unit ${unit}s}
        lappend _templist "$nunit $unit"
        incr _llength
    }

    for {set i $_llength} {$i > 0} {incr i -1} {
        if {$i == $_llength} { 
            set _return "[lindex $_templist $i]"
        } elseif {$i == [expr {$_llength -1}]} { 
            set _return "[lindex $_templist $i] and $_return"
        } else {
            set _return "[lindex $_templist $i], $_return"
        }
    }

    return $_return
}

Test:

[luc]5003> p.spell.seconds 49
49 seconds
[luc]5004> p.spell.seconds 499
8 minutes and 19 seconds
[luc]5005> p.spell.seconds 4999
1 hour, 23 minutes and 19 seconds
[luc]5006> p.spell.seconds 49999
13 hours, 53 minutes and 19 seconds
[luc]5007> p.spell.seconds 499999
5 days, 18 hours, 53 minutes and 19 seconds
[luc]5008> p.spell.seconds 4999999
1 month, 3 weeks, 6 days, 20 hours, 53 minutes and 19 seconds
[luc]5009> p.spell.seconds 49999999
1 year, 7 months, 3 days, 16 hours, 53 minutes and 19 seconds

LES 2022-12-18: Wow, this is embarrassing. I just had to write some code to convert a time lapse in seconds into plain English and thought it would be good to share. Only to arrive here and see myself saying "I should have searched Wikit before trying to make my own." I made and shared my own 13 years ago! Oh well, here goes another reinvented wheel:

proc SecondsToEnglish {lapse}        {

        set days 0
        set hours 0
        set minutes 0
        set seconds 0

        if {$lapse == 86400} {return "1 day"}
        if {$lapse == 3600} {return "1 hour"}
        if {$lapse == 60} {return "1 minute"}
        if {$lapse == 0} {return "No time at all."}

        if {$lapse > 86400}        {
                set days [expr $lapse / 86400]
                set rest [expr $days * 86400]
                set rest [expr $lapse - $rest]
                set lapse $rest
                if {$days == 1} {set days "1 day"}
                if {$days >= 2} {set days "$days days"}
        }

        if {$lapse > 3600}        {
                set hours [expr $lapse / 3600]
                set rest [expr $hours * 3600]
                set rest [expr $lapse - $rest]
                set lapse $rest
                if {$hours == 1} {set hours "1 hour"}
                if {$hours >= 2} {set hours "$hours hours"}
        }

        if {$lapse > 60}        {
                set minutes [expr $lapse / 60]
                set rest [expr $minutes * 60]
                set rest [expr $lapse - $rest]
                set lapse $rest
                if {$minutes == 1} {set minutes "1 minute"}
                if {$minutes >= 2} {set minutes "$minutes minutes"}
        }
        if {$lapse < 60}        {
                set seconds $lapse
                if {$seconds == 1} {set seconds "1 second"}
                if {$seconds >= 2} {set seconds "$seconds seconds"}
        }

        foreach t {days hours minutes seconds}        {
                if {[lindex [set $t] 0] > 0} {lappend units $t}
        }

        if {[llength $units] == 1} {
                return "[set $units]"
        }
        if {[llength $units] == 2} {
                return "[set [lindex $units 0]] and [set [lindex $units 1]]"
        }
        if {[llength $units] == 3} {
                return "[set [lindex $units 0]], [set [lindex $units 1]] and [set [lindex $units 2]]"
        }
        if {[llength $units] == 4} {
                return "[set [lindex $units 0]], [set [lindex $units 1]], [set [lindex $units 2]] and [set [lindex $units 3]]"
        }
}

AM I needed to do some date/time computations and ran into the intricacies of daylight saving schemes. Here is a page about it: Clock and daylight saving time corrections


zxpim