Tuesday, May 20, 2008

How Accurate Is Your Clock?

I bumped my head against another one. While Ruby is not equal in all environments, I at least want to control the difference wherever possible.

When you do a Time.now, you'll get something like
  Tue May 20 17:08:21 -400 2008
Lets say that you're formating that time for a log message though...
  Time.now.strftime "%m/%d/%y %H:%M:%S"
which gives
  05/20/08 17:08:21
Great, except that you'd like subsecond precision.
  (time = Time.now).strftime "%m/%d/%y %H:%M:%S." <<
("%06d" % time.usec)
Well, my iMac gives me six digits of microseconds,
  05/20/08 17:08:21.943286
but alas, my PC only three,
  05/20/08 17:08:21.943000
While you may say, "So what?" I must reply, "Yuck." I just don't want those empty zeros hanging out there. The purist in me wants to lop them off. What I want is
  (time = Time.now).strftime "%m/%d/%y %H:%M:%S." <<
("%06d" % time.usec)[0,@usecs]
where @usecs is the subsecond precision of the clock.

Now, I could just use the Config to get the platform I'm on and assign the correct value, but in this instance I'd like to be more proactive. I can figure out the right value empirically. I start by getting an Array of sample microseconds that are slightly spread out in time.
  t = (1..5).collect { sleep 0.001001; "#{Time.now.usec}" }
Then I figure out what digit contains the last non-zero digit
  nz = t.collect { |s| s.length - (/[1-9]/ =~ s.reverse) }
And finally, I just take the max
  @usecs = nz.max
Why the multiple samples? Because there's still a one in ten chance that a zero will occur naturally in the real non-zero digit position. Or one in one hundred for two zeros, or one in a thousand for three. By running multiple samples and taking the max, we won't be likely fooled, statistically speaking. Why five samples? I just figure that those odds are pretty darned good.

Of course, we can collapse this all nicely,
  class Time
    @@subsecond_precision = nil
    def self.subsecond_precision
@@subsecond_precision ||=
(1..5).collect {
sleep 0.001001
s = "#{Time.now.usec}"
s.length - (/[1-9]/ =~ s.reverse)
Now I can just use Time.subsecond_precision in place of @usecs above. I don't have to worry about using system-dependent assignments. I can just do it once when I need it and move forward.

1 comment:

Alan Roberta said...

Interesting.......I am a clocks manufacturer supplier and looking for partners globally. If any one interest please contact me through this website.