2

I have some code here in TCL that tries to measure the time between two dates.

The two first work, but as you see the last one not working; it counts totally wrong, as there can only be 12 month in a year, but this has more than that.

Anyway, I think that the leap year is the problem, but I'm not sure. Can you help?

proc isTimeAgo {t1} {

  set t2 "1387524660"
  #set t2 [clock seconds]
  set cnt [expr {(($t2 - $t1) / 31536000)}]

  set cur [clock add $t1 $cnt years]

  set res {}

  foreach unit {years months weeks days hours minutes seconds} {
    while {$cur <= $t2} {
      set cur [clock add $cur 1 $unit]
      incr cnt
    }

    set cur [clock add $cur -1 $unit]
    incr cnt -1

    if {$cnt} {
      lappend res $cnt $unit
    }

    set cnt 0
  }

  return $res
}

puts "1: [isTimeAgo "1355988659"]"
puts "2: [isTimeAgo "1355988660"]"
puts "3: [isTimeAgo "1355988661"]"

proc days_per_month year {
  set leap [expr {($year%4)==0 && (!($year%400) || ($year%100))}]
  set days [list 31 [expr {$leap ? 29 : 28}] 31 30 31 30 31 31 30 31 30 31]
  return $days
}

The result of this is:

1: 1 years 1 seconds <- Correct
2: 1 years <- Correct
3: 11 months 4 weeks 1 days 23 hours 59 minutes 59 seconds <- Wrong
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Whiskey
  • 33
  • 3
  • I have de-ja-vous. Did a similar question turn up a hours ago? – Ed Heal Apr 26 '14 at 08:11
  • Why do you say it's wrong? I didn't go through the detailed calculations, but your 3rd line has 11 months, and the remaining part is less than a month. November has 30 days, which is 4 weeks and 2 days and the result kind of makes sense to me. – Jerry Apr 26 '14 at 09:40
  • Jerry, if it its correct i will see it in a different view, as every month is counted in month and every week n week and yeah days in days. The output im after is "1 years 2 seconds" – Whiskey Apr 26 '14 at 12:13
  • @user3575464 Then the first and second would be incorrect by that reasoning. If you increase 1355988660 by 1 second, you cannot expect to get a difference of 2 seconds. And if `1355988659` gave 1 year 1 second, you need to get something smaller than a year, otherwise, the first one should have been 1 year - 1 s, which would be what you got in the third line. – Jerry Apr 26 '14 at 12:19
  • I know. And i think i have make this wrong in my head, as i say its 100% correct, But does it works whit year leap? – Whiskey Apr 26 '14 at 12:21

1 Answers1

2

Date and time arithmetic is astonishingly hard to get right because what people mean by it is so thoroughly uncertain. When adding an interval to a time, you've got to add the years before the months (because of leap years), the months before the days (because of varying month lengths), and the days before the times (because of DST changes). This is sufficiently tricky that we've got a command that handles the complexity: clock add (requires at least Tcl 8.5). Going in the reverse direction? It's a matter of trying each unit until you overshoot.

proc getInterval {from to} {
    set result {}
    foreach unit {year month week day hour minute second} {
        set n 0
        while 1 {
            set new [clock add $from 1 $unit]
            if {$new > $to} break
            set from $new
            incr n
        }
        lappend result $n $unit
    }
    return $result
}

Let's try that out:

% getInterval 1355988659 [clock seconds]
1 year 4 month 0 week 6 day 6 hour 52 minute 39 second

I suppose we could add in ensuring that from precedes to and omit items that are zero. I'll leave those as an exercise.

Be aware that you might need to change the locale and timezone used by clock add to get the answer you are expecting (using the -locale and -timezone options, respectively). See the documentation for exactly what this may affect and some examples.

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • Donal Fellows im not sure how this work, but hopefully you see my notice – Whiskey Apr 26 '14 at 20:48
  • +1 for the opening sentence. One of the tricky issues is "for each of the given dates in {`2013-01-31`, `2013-01-30`, `2013-01-29`, `2013-01-28`, `2012-01-31`, `2012-01-30`, `2012-01-29`, `2012-01-28`}, what date is `given_date + 1 month`?". Adding one month to dates at the end of January is just the simplest case; any date after the 28th of the month plus an appropriate number of months is problematic. In ISO SQL, you can't mix year-month intervals with day-second intervals. In Oracle SQL, the MONTHS_BETWEEN function gives some idiosyncratic answers for pairs of dates around the end of month. – Jonathan Leffler Apr 26 '14 at 21:22
  • @JonathanLeffler That's exactly why I promote `clock add`. For example, `clock format [clock add [clock scan 2013-01-31] 1 month]` gives `Thu Feb 28 00:00:00 GMT 2013`, which is the best approximation of what people want when they add a month on at that point (end of January to end of February). Adding `30 days` would be different. Of course, the `clock` command's implementation is _scary_ code, but you'd expect that. It's also very heavily tested… – Donal Fellows Apr 27 '14 at 08:04
  • But no one really give me an close answer to my question, if it use this correct or not, and if its not do that how exactly do i make it do that. As clock command not is what i know best in TCL – Whiskey Apr 27 '14 at 08:08
  • @Whiskey The core of the answer is that date interval arithmetic _hard_ and `clock add` is the command that knows how to deal with the complexity. You might also need to condition the inputs (with `clock format` and `clock scan`) and it's highly likely that you'll need to specify locale and timezone to get things completely right. The more you know, the more insane it gets; blame politicians (and the rest of humanity) for this mess. – Donal Fellows Apr 27 '14 at 08:15
  • The whole `clock` implementation is an enormous amount of code, with some pieces in C and a huge Tcl file. (128kB of Tcl code? Just for this? Yikes!) However, provided you do your arithmetic right (decreasing unit sizes is the key) then all the existing code _already_ handles the evil edge cases for you; it's the code you've put in to try and handle some of them that is going wrong… – Donal Fellows Apr 27 '14 at 08:19