Velocity Reviews > Ruby > [QUIZ] Fuzzy Time (#99)

# [QUIZ] Fuzzy Time (#99)

Ruby Quiz
Guest
Posts: n/a

 10-27-2006
The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Gavin Kistner

The Background
---------------------------------------------------------------------

Last night I was having trouble falling asleep, and was staring at my digital
alarm clock (as I do so often under that circumstance). Something on the bedside
table was occluding the minutes digit, however, so all I could tell is that it
was "10 4". (Oddly, my alarm clock has no ":" between the hours and minutes.)

"How nice!" I thought. "An imposed vagueness to keep me from obsessing on
exactly what time it is! Should I really be worried about the exact time?
Shouldn't I be more relaxed? Shouldn't a 10-minute precision in life be enough
to keep me roughly on time, without obsessing on exacting promptitude?"

I realized that if I kept staring at the clock (as I did), and I were to observe
it changing from "10 4" to "10 5", that I would, at that moment, know exactly
what time it is.

"Bah" I thought, "so much for that idea."

And then I thought some more. I thought of bad ideas: analog watches where the
hand erratically swings forward and backward, digital clocks that showed random
times near the correct time. And then I dreamed of the watch I wanted to own...

The Challenge
---------------------------------------------------------------------

Requirement #1: Write a Ruby program that shows the current time, but only the
'tens' part of the minutes. For example, if the time is 10:37, then the program
might output "10:3~"

Requirement #2: The time shown by the clock must randomly vary +/- 5 minutes
from reality. For example, if the time is actually 10:37, the program might
output "10:3~" or "10:4~" (but not "10:2~" or "10:5~").

Requirement #3: The time on the clock should continuously increase. If the time
shows "10:4~" it must continue to show "10:4~" until it shows "10:5~". (It can't
show "10:4~", then "10:3~" for a bit and then come back to "10:4~".)

Putting the three requirements together, the left column below shows the actual
time and the next three columns show the possible outputs from three different
runs of the same program:

10:35 10:3~ 10:4~ 10:3~
10:36 10:3~ 10:4~ 10:3~
10:37 10:3~ 10:4~ 10:3~
10:38 10:3~ 10:4~ 10:3~
10:39 10:4~ 10:4~ 10:3~
10:40 10:4~ 10:4~ 10:3~
10:41 10:4~ 10:4~ 10:3~
10:42 10:4~ 10:4~ 10:3~
10:43 10:4~ 10:4~ 10:3~
10:44 10:4~ 10:4~ 10:3~
10:45 10:4~ 10:4~ 10:4~
10:46 10:4~ 10:4~ 10:5~
10:47 10:4~ 10:4~ 10:5~
10:48 10:4~ 10:4~ 10:5~
10:49 10:4~ 10:4~ 10:5~
10:50 10:4~ 10:4~ 10:5~
10:51 10:4~ 10:4~ 10:5~
10:52 10:5~ 10:4~ 10:5~
10:53 10:5~ 10:4~ 10:5~
10:54 10:5~ 10:4~ 10:5~
10:55 10:5~ 10:5~ 10:5~
10:56 10:5~ 10:5~ 11:0~
10:57 10:5~ 10:5~ 11:0~
10:58 10:5~ 10:5~ 11:0~
10:59 10:5~ 10:5~ 11:0~
11:00 10:5~ 10:5~ 11:0~
11:01 10:5~ 10:5~ 11:0~

---------------------------------------------------------------------

You should supply a FuzzyTime class that supports the following:

ft = FuzzyTime.new # Start at the current time
ft = FuzzyTime.new(Time.at(1161104503)) # Start at a specific time

p ft.to_s # to_s format
#=> "10:5~"

p ft.actual, ft.actual.class # Reports real time as Time
#=> Tue Oct 17 11:01:36 -0600 2006
#=> Time

puts ft # by a specified number of
#=> 11:0~ # seconds.

sleep( 60 * 10 )

ft.update # Automatically update the time based on the
puts ft # time that has passed since the last call
#=> 11:1~ # to #initialize, #advance or #update

Your class and output will be tested with code like the following:

def test_output
# Initialize with a well-known time
ft = FuzzyTime.new( Time.at( ... ) )

60.times do
@legal = ... # Find the array of legal output strings
@output = ft.to_s

assert_block "#@output not one of #@legal.inspect" do
@legal.include?( @output )
end

sleep( rand( 30 ) )
ft.update
end

60.times do
@legal = ... # Find the array of legal output strings
@output = ft.to_s

assert_block "#@output not one of #@legal.inspect" do
@legal.include?( @output )
end

end
end

Extra Credit
---------------------------------------------------------------------

* Provide a self-running application that shows the time somehow.
(ASCII in the terminal, some GUI window, etc.)

* Allow your class to be customized to display 12- or 24-hour time.

* Allow your class to be customized to change how close to reality
it must display. (+/- 3 minutes, +/- 12 minutes, etc.)

* Allow your class to be customized to change how roughly it displays
the time (e.g. 1 minute, 10 minute, 1 hour intervals).

* Ensure that the transition from one digit to the next occurs
randomly across the range of -5 to +5. (So, while the digit might
change 5 minutes before or 5 minutes after the real transition, on
average the change should occur around the transition itself.)
called with a certain regularity (e.g. once per second, once every
7 seconds, once every 30 seconds, etc.)

* Come up with your own technique of displaying time that
(a) is always 'close' to right, but (b) never allows a
watchful person to ever know exactly what time it is.

Things to Keep in Mind
---------------------------------------------------------------------

* You need to be able to handle the transition across hour/day
boundaries. (10:5~ might change to 11:0~ when the real time is still
10:58, or might continue to display 10:5~ when the real time is
11:04. On a 24-hour click, you also need to be able to wrap from
23:5~ to 00:0~)

* For testing purposes of the real-time #update advancing, you might
find it easier to work with minutes and seconds instead of hours and
minutes.

* Requirement #3 is, well, a requirement. Repeated #update/#to_s
calls to a FuzzyTime instance should never show an earlier time
(unless 24 hours occurred between #update calls .

ara.t.howard@noaa.gov
Guest
Posts: n/a

 10-27-2006
On Fri, 27 Oct 2006, Ruby Quiz wrote:

> The Challenge
> ---------------------------------------------------------------------
>
> Requirement #1: Write a Ruby program that shows the current time, but only the
> 'tens' part of the minutes. For example, if the time is 10:37, then the program
> might output "10:3~"
>
> Requirement #2: The time shown by the clock must randomly vary +/- 5 minutes
> from reality. For example, if the time is actually 10:37, the program might
> output "10:3~" or "10:4~" (but not "10:2~" or "10:5~").
>
> Requirement #3: The time on the clock should continuously increase. If the time
> shows "10:4~" it must continue to show "10:4~" until it shows "10:5~". (It can't
> show "10:4~", then "10:3~" for a bit and then come back to "10:4~".)

it seems like #2 and #3 contradict one another. imagine it's 10:45 and,
through randomness, you choose to vary the clock by +5, therefore displaying
10:5~, you will not be able to change the output again until 10:55, and then
only because the upper bould will have rolled over into the next hour. here it
is in table form

----- ------ ----- ---------
lower actual upper selection
----- ------ ----- ---------
10:40 10:45 10:50 10:50/5~
10:41 10:46 10:51 10:51/5~
10:42 10:47 10:52 10:52/5~
10:43 10:48 10:53 10:53/5~
10:44 10:49 10:54 10:54/5~
10:45 10:50 10:55 10:55/5~
10:46 10:51 10:56 10:56/5~
10:47 10:52 10:57 10:57/5~
10:48 10:53 10:58 10:58/5~
10:49 10:54 10:59 10:59/5~
10:50 10:55 11:00 11:00/0~

an initial high selection eats into subsequent ranges - in otherwords a
selection of t+ means that #2 will not by able to hold: the variance will be
required to b smaller for the next choice: a high selection eats the variance
range of the next choice...

it's possibly even worse when we not near the top of an hour and start of with
maximum variance:

----- ------ ----- ---------
lower actual upper selection
----- ------ ----- ---------
10:20 10:25 10:30 10:30/3~
10:21 10:26 10:31 10:31/3~
10:22 10:27 10:32 10:32/3~
10:23 10:28 10:33 10:33/3~
10:24 10:29 10:34 10:34/3~
10:25 10:30 10:35 10:35/3~
10:26 10:31 10:36 10:36/3~
10:27 10:32 10:37 10:37/3~
10:28 10:33 10:38 10:38/3~
10:29 10:34 10:39 10:39/3~
10:30 10:35 10:40 10:30/3~
10:31 10:36 10:41 10:31/3~
10:32 10:37 10:42 10:32/3~
10:33 10:38 10:43 10:33/3~
10:34 10:39 10:44 10:34/3~
10:35 10:40 10:45 10:35/3~
10:36 10:41 10:46 10:36/3~
10:37 10:42 10:47 10:37/3~
10:38 10:43 10:48 10:38/3~
10:39 10:44 10:49 10:39/3~
10:40 10:45 10:50 10:50/5~

so the combined effect means that it's acceptable to display the same time~
for twenty straight minutes - is that really the a desired potential effect?

-a
--
my religion is very simple. my religion is kindness. -- the dalai lama

Louis J Scoras
Guest
Posts: n/a

 10-27-2006
On 10/27/06, http://www.velocityreviews.com/forums/(E-Mail Removed) <(E-Mail Removed)> wrote:
>
> it seems like #2 and #3 contradict one another.

Indeed. I take it that the challenge of the quiz is to come up with a
way to balance out the two goals. The purpose for requirement 2 is
that you don't want the user to be able to deduce the actual time
based on when you switch from one output to the next. The problem
with doing that as a simple random selection is that you get wildly
differing ranges.

> +/- 5 minutes from reality.

Is problematic, as you point out Ara. The example

> For example, if the time is actually 10:37, the program might
> output "10:3~" or "10:4~" (but not "10:2~" or "10:5~").

however, is much more lenient. You don't have to round down, you just
can't pick a number from a different section. (10:3~ is not +/-5 of
10:37 obviously).

I gather that there would be some pretty simple ways you can dampen
the latter effect while still preventing the user from easily guessing
the actual time.

--
Lou.

Louis J Scoras
Guest
Posts: n/a

 10-29-2006
#!/usr/bin/env ruby
################################################## ##############################
# quiz99.rb: Implementation of RubyQuiz #99 - Fuzzy Time
#
# Lou Scoras <(E-Mail Removed)>
# Sunday, October 29th, around 11 o-clock (*smirks*).
#
# I decided to approximate time to the quarter hour. When people on the street
# bother you for the time, that's probably what they are expecting. It's been
# my experience that they give you nasty looks when you make them do any
# amount of trivial math =)
#
# This one was a lot of fun. Thanks again to James and Gavin for another great
# quiz.
#
################################################## ##############################

class Time

# A string representation for times on the quarter hour

def to_quarter_s
case min
when 0
"#{hour_12} o-clock"
when 15
"quarter past #{hour_12}"
when 30
"half past #{hour_12}"
when 45
n = self + 3600
"quarter 'till #{n.hour_12}"
end
end

# Let the Time library to the hard work for getting 12 time.

def hour_12
(strftime "%I").sub(/\A0/,'')
end
end

class FuzzyTime

# Quarter hours fall every 900 seconds

TimeScale = 60 * 15

def initialize(time = Time.now)
@real = @time = time
@emitted = nil
end

# Use a simple linear scaling to even out the intervals. This seems to work
# out okay after a little testing, but it could probably be improved quite a
# bit.
#
# One variable that effects this is the sampling rate. We're approximating to
# the nearest quarter hour; however, if you call to_s say once a second, you
# have a greater chance of bumping up to the next interval than if you
# increase the time between calls to one minute.

def to_s
pick = rand(TimeScale)
threshold = TimeScale - @time.to_i % TimeScale - 1
p [pick, threshold, last_valid, next_valid] if \$DEBUG

@emitted = if (!@emitted.nil? && @emitted > last_valid) \
|| pick >= threshold
next_valid
else
last_valid
end

@emitted.to_quarter_s
end

def inspect
t_obj = if @emitted.nil?
self.class.new(@time).next_valid
else
@emitted
end
%!FuzzyTime["#{t_obj.to_quarter_s}"]!
end

def actual
@time
end

def last_valid
Time.at(@time.to_i / TimeScale * TimeScale)
end

def next_valid
last_valid + TimeScale
end

@real = Time.now
@time += offset
offset
end

def update
now = Time.now
delta = now - @real
@time += delta
@real = now
end

end

# Err, I forgot what the incantation is on windows. I think it is 'cls', but
# I'll leave it as an exercise to the reader. *winks*

ClearString = `clear`
def clear
print ClearString
end

def get_sample_rate
default = 60
return default unless ARGV[0]
begin
return ARGV[0].to_i
rescue
return default
end
end

if caller.empty?
ft = FuzzyTime.new

while true do
clear
puts ft
sleep get_sample_rate
ft.update
end
end

Marcel Ward
Guest
Posts: n/a

 10-29-2006
Thanks for this week's interesting problem. My solution is below and
I look forward to any feedback and seeing other techniques used.

There are two files pasted below; the second is the unit test, which
takes about 5-10 minutes to run to completion. My first unit test
ever in any language

This is also my first greeting to the Ruby community... Hello!

Cheers,
Marcel

#!/usr/bin/env ruby
#
# Marcel Ward <wardies ^a-t^ gmaildotcom>
# Sunday, 29 October 2006
# Solution for Ruby Quiz number 99
#
################################################
# fuzzy_time.rb

class FuzzyTime

# If the time passed is nil, then keep track of the time now.
def initialize(tm=nil, range_secs=600, disp_accuracy_secs=range_secs,
fmt="%H:%M", obs_period=nil)
@actual = @last_update = @next_diff = tm || Time.now
@realtime = tm.nil?
@maxrange = range_secs
@display_accuracy = @best_accuracy = disp_accuracy_secs
@tformat = fmt
@last_observed = @max_disptime = Time.at(0)
@timed_observation_period = obs_period
end

def to_s
@last_update = Time.now
@actual = @last_update if @realtime

check_observation_period unless @timed_observation_period.nil?

# We only calculate a new offset each time the last offset times out.
if @next_diff <= @actual
# Calculate a new time offset
@diff = rand(@maxrange) - @maxrange/2
# Decide when to calculate the next time offset
@next_diff = @actual + rand(@maxrange)
end
@last_observed = @actual

# Don't display a time less than the time already displayed
@max_disptime = [@max_disptime, @actual + @diff].max

# Take care to preserve any specific locale (time zone / dst) information
# stored in @actual - for example, we cannot use Time::at(Time::to_i).
disptime = @max_disptime.strftime(@tformat)

# Lop off characters from the right of the display string until the
# remaining string matches one of the extreme values; then fuzz out the
# rightmost digits
(0..disptime.size).to_a.reverse.each do
|w|
[@display_accuracy.div(2), - @display_accuracy.div(2)].map{
|offs|
(@max_disptime + offs).strftime(@tformat)
}.each do
|testtime|
return disptime[0,w] + disptime[w..-1].tr("0123456789", "~") if \
disptime[0,w] == testtime[0,w]
end
end
end

if @realtime
@actual = Time.now + secs
# Once a real-time FuzzyTime is advanced, it can never again be
# real-time.
@realtime = false
else
@actual += secs
end
@last_update = Time.now
end

def update
diff = Time.now - @last_update
@actual += diff
@last_update += diff
# By calling update, you are effectively saying "set a fixed time"
# so we must disable the real-time flag.
@realtime = false
end

def accuracy
"+/- #{@maxrange/2}s"
end

def dump
"actual: #{@actual.strftime("%Y-%m-%d %H:%M:%S")}, " \
"diff: #{@diff}, " \
"next_diff: #{@next_diff.strftime("%Y-%m-%d %H:%M:%S")}, " \
"accuracy: #{@display_accuracy}"
end

private
def check_observation_period
# Is the clock being displayed too often?

# Although this method seems to work, it may be a bit simplistic.
# Proper statistical / mathematical analysis and a proper understanding
# of the human ability to count seconds may be necessary to determine
# whether this still gives away too much info for the average observer.

patience = @actual - @last_observed

if patience < @timed_observation_period / 2
# Worsen display accuracy according to how impatient the observer is.
@display_accuracy += (2 * @best_accuracy *
(@timed_observation_period - patience)) /
@timed_observation_period
elsif patience < @timed_observation_period
# Immediately punish impatience by enforcing a minumum accuracy
# twice as bad as the best possible.
# Don't give too much away but allow the accuracy to get slowly better
# if the observer is a bit more patient and waits over half the
# observation period
@display_accuracy = [
2 * @best_accuracy,
@display_accuracy - ((@best_accuracy * patience) /
@timed_observation_period)
].max
else
# The observer has waited long enough.
# Reset to the best possible accuracy.
@display_accuracy = @best_accuracy
end
end
end

def wardies_clock
# Get us a real-time clock by initializing Time with first parameter==nil
# Make the seconds harder to guess by expanding the range to +/- 15s whilst
# keeping the default display accuracy to +/- 5 secs. The user will have
# to wait 30s between observations to see the clock with best accuracy.
ft = FuzzyTime.new(nil, 30, 10, "%H:%M:%S", 30)

# This simpler instantiation does not check the observation period and
# shows "HH:M~". (This is the default when no parameters are provided)
#ft = FuzzyTime.new(nil, 600, 600, "%H:%M")

puts "** Wardies Clock\n"
puts "**\n** Observing more often than every " \
"#{ft.timed_observation_period} seconds reduces accuracy" \
unless ft.timed_observation_period.nil?
puts "**\n\n"

loop do
puts "\n\nTime Now: #{ft.to_s} (#{ft.accuracy})\n\n" \
"-- Press Enter to observe the clock again or " \
"q then Enter to quit --\n\n"

# Flush the output text so that we can scan for character input.
STDOUT.flush

break if STDIN.getc == ?q
end
end

def clocks_go_back_in_uk
# Clocks go back in the UK on Sun Oct 29. (+0100 => +0000)
# Start at Sun Oct 29 01:58:38 +0100 2006
ft = FuzzyTime.new(Time.at(Time.at(116208351))

# In the UK locale, we see time advancing as follows:
# 01:5~
# 01:5~
# 01:0~ (clocks gone back one hour)
# 01:0~
# ...
# 01:0~
# 01:1~

60.times do
puts ft.to_s
end
end

def full_date_example
# Accuracy can be set very high to fuzz out hours, days, etc.
# E.g. accuracy of 2419200 (28 days) fuzzes out the day of the month
# Note the fuzz factoring does not work so well with hours and
# non-30-day months because these are not divisble exactly by 10.

tm = FuzzyTime.new(nil, 2419200, 2419200, "%Y-%m-%d %H:%M:%S")
300.times do
puts "#{tm.to_s} (#{tm.dump})"
#sleep 0.2
end
end

# Note, all the examples given in the quiz are for time zone -0600.
# If you are in a different timezone, you should see other values.
def quiz_example
ft = FuzzyTime.new # Start at the current time
ft = FuzzyTime.new(Time.at(1161104503)) # Start at a specific time

p ft.to_s # to_s format

p ft.actual, ft.actual.class # Reports real time as Time
#=> Tue Oct 17 11:01:36 -0600 2006
#=> Time

puts ft # by a specified number of
#=> 11:0~ # seconds.

sleep( 60 * 10 )

ft.update # Automatically update the time based on the
puts ft # time that has passed since the last call
#=> 11:1~ # to #initialize, #advance or #update
end

if __FILE__ == \$0
wardies_clock
#clocks_go_back_in_uk
#full_date_example
#quiz_example
end

################################################
# fuzzy_time_test.rb

require 'test/unit'
require 'fuzzy_time'

class FuzzyTime_Test < Test::Unit::TestCase
#def setup
#end

#def teardown
#end

# Initialize with a known UTC time (Tue Jun 10 03:14:52 UTC 1975)
ft = FuzzyTime.new(Time.at(171602092).getgm, 60, 60, "%H:%M:%S")

# Add 6 hours 45 minutes 30 secs to give us
# (Tue Jun 10 09:59:22 UTC 1975)
@last_output = ""

60.times do
# Initial displayed time sourced from between 09:58:52 and 09:59:52
# Time will be advanced by between 0 and 600 seconds.
# So final displayed time source ranges from 10:08:52 to 10:09:52

# The array of legal output strings:
@legal = ["09:58:~~", "09:59:~~", "10:00:~~", "10:01:~~",
"10:02:~~", "10:03:~~", "10:04:~~", "10:05:~~",
"10:06:~~", "10:07:~~", "10:08:~~", "10:09:~~"]

@output = ft.to_s

assert_block "#@output not one of #{@legal.inspect}" do
@legal.include?( @output )
end

assert_block "#@output must be greater than or equal to " \
"last value, #@last_output" \
do
@output >= @last_output
end
@last_output = @output

end
end

# Initialize with a known UTC time (Fri Dec 31 23:58:25 UTC 1999)
# Test rollover at midnight
# Note, we have an accuracy of +/- 5 secs now and enabled the
# observations timer
ft = FuzzyTime.new(Time.at(946684705).getgm, 10, 10, "%H:%M:%S", 10)

30.times do
# Initial displayed time sourced from between 23:58:20 and 23:58:30
# Time will be advanced by between 0 and 150 seconds.
# So final displayed time source ranges from 00:00:50 to 00:01:00

# Note, if we watch too often over a short period of time,
# our displayed accuracy will decrease. Then we will lose
# the 10's digit of the seconds and occasionally the 1's minute.

# The array of legal output strings:
@legal = ["23:58:1~", "23:58:2~", "23:58:3~",
"23:58:4~", "23:58:5~", "23:58:6~",
"23:58:~~", "23:59:~~", "23:5~:~~",
"23:59:0~", "23:59:1~", "23:59:2~",
"23:59:3~", "23:59:4~", "23:59:5~",
"00:00:0~", "00:00:1~", "00:00:2~",
"00:00:3~", "00:00:4~", "00:00:5~", "00:00:~~",
"00:01:0~", "00:01:~~", "00:0~:~~"]

@output = ft.to_s

assert_block "#@output not one of #{@legal.inspect}" do
@legal.include?( @output )
end

# We cannot easily check that the current output is greater or equal to
# the last because with timed observations, a valid output sequence is:
# 23:59:0~
# 23:59:~~ (looking too often, accuracy has been reduced)
# 23:59:0~ (waited long enough before observing for accuracy to return)

end
end

def test_update
# NOTE - this test takes 5-10 minutes to complete

# Initialize with a known UTC time (Tue Jun 10 03:14:52 UTC 1975)
ft = FuzzyTime.new(Time.at(171602092).getgm, 60, 60, "%H:%M:%S")
@last_output = ""

60.times do
# Initial displayed time sourced from between 03:14:22 and 03:15:22
# Duration of loop will be between 0 and ~600 seconds.
# So final displayed time source ranges from 03:14:22 to 03:25:22

# The array of legal output strings:
@legal = ["03:14:~~", "03:15:~~", "03:16:~~", "03:17:~~",
"03:18:~~", "03:19:~~", "03:20:~~", "03:21:~~",
"03:22:~~", "03:23:~~", "03:24:~~", "03:25:~~"]

@output = ft.to_s

assert_block "#@output not one of #{@legal.inspect}" do
@legal.include?( @output )
end

assert_block "#@output must be greater than or equal to " \
"last value, #@last_output" \
do
@output >= @last_output
end
@last_output = @output

sleep( rand( 11 ) ) # wait between 0..10 secs
ft.update
end
end
end

James Edward Gray II
Guest
Posts: n/a

 10-29-2006
On Oct 29, 2006, at 10:28 AM, Marcel Ward wrote:

> This is also my first greeting to the Ruby community... Hello!

Then let me be one of the first to welcome you to Ruby and the Ruby
Quiz!

James Edward Gray II

Paolo Negri
Guest
Posts: n/a

 10-29-2006
Hi all

Here's is my script. It was fun working at it, just please no more
time based quiz when there's the switching of daylight time saving
The scripts defines two class one to manage how to move within a given
range and one for actual FuzzyTime implementation.
At the end of the email there's a really ugly extra file you may want
to use to play with the FuzzyTime class.

Paolo

#!/bin/ruby
class WanderingWalker
SIGN = [1, -1]
CHECK_LIMIT = %w(min max)
DEFAULT_GENERATOR = lambda { |limit| rand(limit) * SIGN[rand(2)] }
alias :level osition
def initialize(limit, &block)
@limit = limit
@generator = block_given? ? block : DEFAULT_GENERATOR
@position = 0
generate_target
end
def walk(steps)
generate_target while @position == @target
new_pos = [(@position + direction*steps), @target].send \
CHECK_LIMIT[SIGN.index(direction)]
@position = new_pos
end
def distance
@target - @position
end
def direction
if distance == 0 : 1 else distance/distance.abs end
end
private
def generate_target
@target = @generator.call limit
end
end

class FuzzyTime
FORMATS = %w(%H:%M %I:%M)
#two params, both optional time and an hash
#time: a time object used as the starting point
#hash params
#:hidden_digits - hidden_digits number, from right
#recision - maximum distance from real time in seconds
def initialize(time=Time.new, opts={})
@internaltime = time
@last_call = @printed_time = Time.new
@precision = opts[recision] || 300
@fuzziness = WanderingWalker.new(@precision)
@format = FORMATS[0]
@hidden_digits = opts[:hidden_digits] || 1
@sub_args = case @hidden_digits
when 0 : [//, '']
when 1 : [/\d\$/, '~']
when 2 : [/\d{2}\$/, '~~']
when 3 : [/\d:\d{2}\$/, '~:~~']
else
raise "nothing to see!"
end
end
tic(secs)
end
def update
tic Time.new - @last_call
end
def actual
@internaltime
end
#switch 12 / 24h format
def toggle_format
@format = FORMATS[FORMATS.index(@format) == 0 ? 1 : 0]
end
def to_s
@printed_time = [@printed_time, (@internaltime + fuzziness.level)].max
@printed_time.strftime(@format).sub(*@sub_args)
end
private
def tic(secs=1)
@internaltime += secs
@last_call = Time.new
@fuzziness.walk secs
end
end

Ad here's the super ugly runner script

#!/bin/ruby
require 'fuzzy_time'
# accepts a FuzzyTime object as argument + an hash of options
#possible options
# :time_warp if true instead of working as a clock just prints out
#in an instant all the clock output, if false, works as a clock
# :step_size how often the clock will be updated in seconds
# rint_actual prints the actual time
# rint_fuzziness, prints the error size
# :toggle switch at every step from 24 to 12 hour format
# :duration how many step_size the clock should run
class FuzzyExec
def initialize(ft = FuzzyTime.new ,opts = {})
opt_def = {:duration => 100, :step_size => 60, rint_actual => true}
@opts = opt_def.update opts
@ft = ft
end
def show_fuzzy_clock
@opts[:duration].times do
@ft.toggle_format if @opts[:toggle]
out = []
out << @ft.to_s
out << @ft.actual.strftime('%H:%M') if @opts[rint_actual]
out << @ft.fuzziness.level if @opts[rint_fuzziness]
out << Time.new.strftime('%H:%M') if @opts[rint_current]
puts out.join ' '
if @opts[:time_warp]
else
sleep @opts[:step_size]
@ft.update
end
end
end
end

Paolo Negri
Guest
Posts: n/a

 10-29-2006
Hi

I've posted an old version the second line of initialize should be changed from

@last_call = @printed_time = Time.new

to

@last_call = @printed_time = time

sorry

Paolo

Ben Giddings
Guest
Posts: n/a

 10-29-2006
Here's my effort. I didn't get around to testing it, but the concept
seems sound. I decided to interpret the goal a little more
conservatively and instead of just making sure the displayed time
never goes forward, I decided to make sure the offset never goes
backwards. To do this, I made the max delta from the current offset
no more than the amount of time that passed since the last offset
update:

class FuzzyTime
MAX_OFFSET = 5*60

def initialize(*args)
now = Time.new
@internal_time = args[0] || now
@time_offset = now - @internal_time
@fuzzy_secs = 0
@last_calc = Time.new
end

def actual
@internal_time
end

def update
@internal_time = Time.new + @time_offset
end

@time_offset += amount.to_i
end

def calc_offset
# Choose a new offset that's between +/- 5 mins. If it has been
# less than 5 mins since the last offset calc, choose that time as
# a max delta (this makes sure time is always going forward)

time_from_last_calc = (Time.new - @last_calc).to_i

if time_from_last_calc > 0
begin
max_delta = [MAX_OFFSET, time_from_last_calc].min

delta = rand((2*max_delta) + 1) - max_delta
end until (delta + @fuzzy_secs).abs < MAX_OFFSET
@fuzzy_secs += delta

puts "Fuzzy secs now: #{@fuzzy_secs}"

@last_calc = Time.new
end
@fuzzy_secs
end

def get_time
fuzzy_hour = @internal_time.hour
fuzzy_min = @internal_time.min
fuzzy_sec = @internal_time.sec + calc_offset

if fuzzy_sec > 60
fuzzy_sec -= 60
fuzzy_min += 1
end

if fuzzy_sec < 0
fuzzy_sec += 60
fuzzy_min -= 1
end

if fuzzy_min > 60
fuzzy_min -= 60
fuzzy_hour = (fuzzy_hour + 1) % 24
end

if fuzzy_min < 0
fuzzy_min += 60
fuzzy_hour = (fuzzy_hour + 23) % 24
end

[fuzzy_hour, fuzzy_min, fuzzy_sec]
end

def to_s
fuzzy_hour, fuzzy_min, fuzzy_sec = get_time
"#{fuzzy_hour}:#{fuzzy_min / 10}~"
# "#{fuzzy_hour}:#{fuzzy_min / 10}~ (#{fuzzy_hour}:#{"%02d" %
fuzzy_min}:#{"%02d" % fuzzy_sec})"
end

end

if \$0 == __FILE__
t = FuzzyTime.new
10.times do
puts t, Time.new
sleep 10
t.update
end
end

Jeremy Hinegardner
Guest
Posts: n/a

 10-29-2006
--envbJBWh7q8WU6mo
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

Ahh, it feels good to do a ruby quiz again . I've ignored the other
solutions so far so someone else may have taken the same approach.

I took the approach that there are actual 3 times involved, so as to
hopefully avoid as little as possible the range limiting that Ara was

Essentially, the time displayed to the user is all that must be
increase. So long as that is continually moving forward, the times
behind the scene don't really matter. That for me results in 3
different times.

- Actual time
- Fuzzed time : actual time fuzzed within the fuzzy range (+- 5 min default)
- Display time : Fuzzed time 'floored' to a granularity (10 min default)

As a result, my approach has the following algorithm:
- Calculate the actual time (update/advance)
- Calculate a fuzzy range for the new fuzzed time which has
- lower bound = the maximum of the last display time or (actual - fuzz factor minutes)
- upper bound = actual + range
- randomly pick a new time in that range for the fuzzed time
- calculate the new display time off of the fuzzed time

As a result, the Fuzzed time can increase/decrease with respect to
itself, but will always be greater than the displayed time. But overall
the fuzzed time will continue to increase. And the display time is
always increasing.

I also threw in some Extra Credits: 24hour/12hour option, fuzz factor
(changing the +- range) and display granularity(one minute, ten minute
on hour).

Great Quiz!

enjoy,

-jeremy

--
================================================== ======================
Jeremy Hinegardner (E-Mail Removed)

--envbJBWh7q8WU6mo
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="fuzzytime.rb"

#----------------------------------------------------------------------
# Ruby Quiz #99 - Fuzzy Time
#
# Jeremy Hinegardner
#----------------------------------------------------------------------
class FuzzyTime

HOUR_24_FORMAT = "%H:%M"
HOUR_12_FORMAT = "%I:%M"

HOUR_FORMAT_OPTIONS = Hash.new(HOUR_24_FORMAT)
HOUR_FORMAT_OPTIONS[HOUR_12_FORMAT] = HOUR_12_FORMAT
HOUR_FORMAT_OPTIONS[:twelve_hour] = HOUR_12_FORMAT
HOUR_FORMAT_OPTIONS[HOUR_24_FORMAT] = HOUR_24_FORMAT
HOUR_FORMAT_OPTIONS[:twentyfour_hour] = HOUR_24_FORMAT

GRANULARITIES = [ne_minute, :ten_minute, ne_hour]

attr_accessor :time_format
attr_accessor :fuzz_factor
attr_accessor :display_granularity

def initialize(time = Time.now)
@actual = time.dup
@time_format = HOUR_24_FORMAT
@fuzz_factor = 5 * 60
@display_granularity = :ten_minute

# initialize a base history that is minimal to give maximum
# range for calculating a fuzzy time
@fuzzed = @actual - @fuzz_factor
@fuzz_history = [ { :actual => @actual, :fuzzed => @fuzzed, :display => calculate_display_time } ]
calculate_fuzz_time
end

@actual += seconds
calculate_fuzz_time
end

def update
@actual = Time.now
calculate_fuzz_time
end

# return a string representation of the display time which is the time
# with up to the @disiplay_granularity replaced by '~'. when
# ne_minute is the granularity, nothing needs to be done
def to_s
s = @display.strftime(time_format)
case @display_granularity
when :ten_minute
s.chop!
s << "~"
when ne_hour
s.chop!
s.chop!
s << "~~"
end
return s
end

# allow the time format to be set to 24 hour or 12 hour time
def time_format=(format)
@time_format = HOUR_FORMAT_OPTIONS[format]
end

# all the fuzz factor to be set in a range of minutes, no limit
def fuzz_factor=(minutes)
@fuzz_factor = 60 * minutes.abs
end

def fuzz_factor
(@fuzz_factor / 60).to_i
end

def display_granularity=(granularity)
if GRANULARITIES.include?(granularity.to_sym)
@display_granularity = granularity
# need to recalculate this when the granularity is updated
calculate_display_time
else
raise ArgumentError, "display_granularity must be one of #{GRANULARITIES.join(",")}"
end
@display_granularity
end

private

#
# the display time is the fuzzy time converted to a "floor" time
# with a granularity base upon the @display_granularity. For
# example, if the fuzzed time is 13:43 then the display time would
# be:
#
# granularity display
# ========================
# ne_minute 13:43
# :ten_minute 13:40
# ne_hour 13:00
#
def calculate_display_time
case @display_granularity
when ne_minute
min = @fuzzed.min
when :ten_minute
min = (@fuzzed.min / 10) * 10
when ne_hour
min = 0
end

@display = Time.mktime( @fuzzed.year, @fuzzed.month, @fuzzed.day, @fuzzed.hour, min, 0, 0)
end

#
# calculate the new fuzzy time.
#
# Since :
# 1) the displayed time must appear to be continually increasing
# 2) we must always be within the fuzz factor of the actual time
#
# Therefore:
# the lower bound of the fuzzy range is the maximum of the displayed
# time or the lower bond of the fuzz factor around the actual time.
#
def calculate_fuzz_time
last_display = @fuzz_history.last[:display]
min_fuzz_factor = @actual - @fuzz_factor
lower_bound = last_display > min_fuzz_factor ? last_display : min_fuzz_factor

upper_bound = @actual + @fuzz_factor

range = upper_bound - lower_bound
@fuzzed = lower_bound + rand(range + 1)
calculate_display_time
@fuzz_history << { :actual => @actual, :fuzzed => @fuzzed, :display => @display }
end

end

--envbJBWh7q8WU6mo--