von 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ module Von
2
+ module Counters
3
+ module Commands
4
+ def hget(*args)
5
+ Von.connection.hget(*args)
6
+ end
7
+
8
+ def hgetall(*args)
9
+ Von.connection.hgetall(*args)
10
+ end
11
+
12
+ def hincrby(*args)
13
+ Von.connection.hincrby(*args)
14
+ end
15
+
16
+ def hset(*args)
17
+ Von.connection.hset(*args)
18
+ end
19
+
20
+ def lrange(*args)
21
+ Von.connection.lrange(*args)
22
+ end
23
+
24
+ def rpush(*args)
25
+ Von.connection.rpush(*args)
26
+ end
27
+
28
+ def llen(*args)
29
+ Von.connection.llen(*args)
30
+ end
31
+
32
+ def lpop(*args)
33
+ Von.connection.lpop(*args)
34
+ end
35
+
36
+ def hdel(*args)
37
+ Von.connection.hdel(*args)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ module Von
2
+ module Counters
3
+ class Current
4
+ include Von::Counters::Commands
5
+
6
+ # Initialize a new Counter
7
+ #
8
+ # field - counter field name
9
+ def initialize(field, periods = nil)
10
+ @field = field.to_sym
11
+ @periods = periods || []
12
+ end
13
+
14
+ # Returns the Redis hash key used for storing counts for this Counter
15
+ def hash_key
16
+ "#{Von.config.namespace}:counters:currents:#{@field}"
17
+ end
18
+
19
+ def current_timestamp(time_unit)
20
+ hget("#{hash_key}:#{time_unit}", 'timestamp')
21
+ end
22
+
23
+ def increment
24
+ return if @periods.empty?
25
+
26
+ @periods.each do |period|
27
+ if period.timestamp != current_timestamp(period.time_unit)
28
+ hset("#{hash_key}:#{period.time_unit}", 'total', 1)
29
+ hset("#{hash_key}:#{period.time_unit}", 'timestamp', period.timestamp)
30
+ else
31
+ hincrby("#{hash_key}:#{period.time_unit}", 'total', 1)
32
+ end
33
+ end
34
+ end
35
+
36
+ def count(time_unit)
37
+ count = hget("#{hash_key}:#{time_unit}", 'total')
38
+ count.nil? ? 0 : count.to_i
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ module Von
2
+ module Counters
3
+ class Period
4
+ include Von::Counters::Commands
5
+
6
+ def initialize(field, periods = nil)
7
+ @field = field.to_sym
8
+ @periods = periods || []
9
+ end
10
+
11
+ # Returns the Redis hash key used for storing counts for this Period
12
+ def hash_key(time_unit)
13
+ "#{Von.config.namespace}:counters:#{@field}:#{time_unit}"
14
+ end
15
+
16
+ # Returns the Redis list key used for storing current "active" counters
17
+ def list_key(time_unit)
18
+ "#{Von.config.namespace}:lists:#{@field}:#{time_unit}"
19
+ end
20
+
21
+ def increment
22
+ return if @periods.empty?
23
+
24
+ @periods.each do |period|
25
+ _hash_key = hash_key(period.time_unit)
26
+ _list_key = list_key(period.time_unit)
27
+
28
+ hincrby(_hash_key, period.timestamp, 1)
29
+
30
+ unless lrange(_list_key, 0, -1).include?(period.timestamp)
31
+ rpush(_list_key, period.timestamp)
32
+ end
33
+
34
+ if llen(_list_key) > period.length
35
+ expired_counter = lpop(_list_key)
36
+ hdel(_hash_key, expired_counter)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Count the fields for the given time period for this Counter.
42
+ #
43
+ # Returns an Array of Hashes representing the count
44
+ def count(time_unit)
45
+ return if @periods.empty?
46
+
47
+ counts = []
48
+ this_period = nil
49
+ _period = @periods.select { |p| p.time_unit == time_unit }.first
50
+
51
+ _period.length.times do |i|
52
+ this_period = _period.prev(i)
53
+ counts.unshift(this_period)
54
+ end
55
+
56
+ keys = hgetall(hash_key(time_unit))
57
+ counts.map { |date| { :timestamp => date, :count => keys.fetch(date, 0).to_i }}
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ module Von
2
+ module Counters
3
+ class Total
4
+ include Von::Counters::Commands
5
+ attr_reader :field
6
+
7
+ # Initialize a new Counter
8
+ #
9
+ # field - counter field name
10
+ def initialize(field)
11
+ @field = field.to_sym
12
+ end
13
+
14
+ # Returns the Redis hash key used for storing counts for this Counter
15
+ def hash_key
16
+ "#{Von.config.namespace}:counters:#{@field}"
17
+ end
18
+
19
+ # Increment the total count for this Counter
20
+ # If the key has time periods specified, increment those.
21
+ #
22
+ # Returns the Integer total for the key
23
+ def increment
24
+ hincrby(hash_key, 'total', 1).to_i
25
+ end
26
+
27
+ # Count the "total" field for this Counter.
28
+ #
29
+ # Returns an Integer count
30
+ def count
31
+ count = hget(hash_key, 'total')
32
+ count.nil? ? 0 : count.to_i
33
+ end
34
+
35
+ # Lookup the count for this Counter in Redis.
36
+ # If a Period argument is given we lookup the count for
37
+ # all of the possible units (not expired), zeroing ones that
38
+ # aren't set in Redis already.
39
+ #
40
+ # period - A Period to lookup
41
+ #
42
+ # Returns an Integer representing the count or an Array of counts.
43
+ def self.count(field)
44
+ Counter.new(field).count
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -1,58 +1,79 @@
1
1
  module Von
2
2
  class Period
3
- AVAILABLE_PERIODS = [ :hourly, :daily, :weekly, :monthly, :yearly ]
3
+ PERIOD_MAPPING = {
4
+ :minutely => :minute,
5
+ :hourly => :hour,
6
+ :daily => :day,
7
+ :weekly => :week,
8
+ :monthly => :month,
9
+ :yearly => :year
10
+ }
11
+ AVAILABLE_PERIODS = PERIOD_MAPPING.keys
12
+ AVAILABLE_TIME_UNITS = PERIOD_MAPPING.values
4
13
 
5
- attr_reader :counter_key
14
+ attr_reader :name
6
15
  attr_reader :length
7
16
  attr_reader :format
8
17
 
9
18
  # Initialize a Period object
10
19
  #
11
- # counter - the field name for the counter
12
20
  # period - the time period one of AVAILABLE_PERIODS
13
21
  # length - length of period
14
- def initialize(counter_key, period, length)
15
- @counter_key = counter_key
16
- @period = period
17
- @length = length
18
- @format = Von.config.send(:"#{@period}_format")
22
+ def initialize(period, length = nil)
23
+ name = period.to_sym
24
+ if AVAILABLE_PERIODS.include?(name)
25
+ @name = name
26
+ elsif AVAILABLE_TIME_UNITS.include?(name)
27
+ @name = PERIOD_MAPPING.invert[name]
28
+ else
29
+ raise ArgumentError, "`#{period}' is not a valid period"
30
+ end
31
+ @length = length
32
+ @format = Von.config.send(:"#{@name}_format")
19
33
  end
20
34
 
21
35
  # Returns a Symbol representing the time unit
22
36
  # for the current period.
23
37
  def time_unit
24
- @time_unit ||= case @period
25
- when :hourly
26
- :hour
27
- when :daily
28
- :day
29
- when :weekly
30
- :week
31
- when :monthly
32
- :month
33
- when :yearly
34
- :year
35
- end
38
+ @time_unit ||= PERIOD_MAPPING[@name]
36
39
  end
37
40
 
38
41
  # Returns True or False if the period is hourly
39
42
  def hours?
40
- @period == :hourly
43
+ @name == :hourly
44
+ end
45
+
46
+ # Returns True or False if the period is minutely
47
+ def minutes?
48
+ @name == :minutely
49
+ end
50
+
51
+ def beginning(time)
52
+ if minutes?
53
+ time.change(:seconds => 0)
54
+ else
55
+ time.send(:"beginning_of_#{time_unit}")
56
+ end
57
+ end
58
+
59
+ def prev(unit = 1)
60
+ beginning(unit.send(time_unit.to_sym).ago).strftime(@format)
61
+ end
62
+
63
+ def timestamp
64
+ beginning(Time.now).strftime(format)
41
65
  end
42
66
 
43
- # Returns the Redis hash key used for storing counts for this Period
44
- def hash_key
45
- @hash ||= "#{Von.config.namespace}:counters:#{@counter_key}:#{@period}"
67
+ def self.unit_to_period(time_unit)
68
+ PERIOD_MAPPING.invert[time_unit]
46
69
  end
47
70
 
48
- # Returns the Redis list key used for storing current "active" counters
49
- def list_key
50
- @list ||= "#{Von.config.namespace}:lists:#{@counter_key}:#{@period}"
71
+ def self.exists?(period)
72
+ AVAILABLE_PERIODS.include?(period)
51
73
  end
52
74
 
53
- # Returns the Redis field representation used for storing the count value
54
- def field
55
- Time.now.strftime(format)
75
+ def self.time_unit_exists?(time_unit)
76
+ AVAILABLE_TIME_UNITS.include?(time_unit)
56
77
  end
57
78
  end
58
79
  end
@@ -1,3 +1,3 @@
1
1
  module Von
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -18,24 +18,44 @@ describe Von::Config do
18
18
  @config.namespace.must_equal 'something'
19
19
  end
20
20
 
21
- it 'stores counter options per key and retrieves them' do
22
- options = { :monthly => 3, :total => false }
23
-
24
- @config.counter 'bar', options
21
+ it "sets periods via counter method" do
22
+ Von.configure do |config|
23
+ config.counter 'bar', :monthly => 3, :daily => 6
24
+ end
25
25
 
26
- @config.namespace.must_equal 'von'
27
- @config.counter_options('bar').must_equal options
26
+ Von.config.periods[:bar].length.must_equal 2
27
+ Von.config.periods[:bar].first.name.must_equal :monthly
28
+ Von.config.periods[:bar].first.length.must_equal 3
29
+ Von.config.periods[:bar].last.name.must_equal :daily
30
+ Von.config.periods[:bar].last.length.must_equal 6
28
31
  end
29
32
 
30
- it "allows config options to be updated via configure" do
31
- options = { :monthly => 3, :total => false }
33
+ it "sets bests via counter method" do
34
+ Von.configure do |config|
35
+ config.counter 'bar', :best => :day
36
+ config.counter 'foo', :best => [ :month, :year ]
37
+ end
38
+
39
+ Von.config.bests[:bar].first.must_be_instance_of Von::Period
40
+ Von.config.bests[:bar].first.name.must_equal :daily
41
+ Von.config.bests[:foo].first.must_be_instance_of Von::Period
42
+ Von.config.bests[:foo].first.name.must_equal :monthly
43
+ Von.config.bests[:foo].last.must_be_instance_of Von::Period
44
+ Von.config.bests[:foo].last.name.must_equal :yearly
45
+ end
32
46
 
47
+ it "sets currents via counter method" do
33
48
  Von.configure do |config|
34
- config.counter 'bar', options
49
+ config.counter 'bar', :current => :day
50
+ config.counter 'foo', :current => [ :month, :year ]
35
51
  end
36
52
 
37
- Von.config.namespace.must_equal 'von'
38
- Von.config.counter_options('bar').must_equal options
53
+ Von.config.currents[:bar].first.must_be_instance_of Von::Period
54
+ Von.config.currents[:bar].first.name.must_equal :daily
55
+ Von.config.currents[:foo].first.must_be_instance_of Von::Period
56
+ Von.config.currents[:foo].first.name.must_equal :monthly
57
+ Von.config.currents[:foo].last.must_be_instance_of Von::Period
58
+ Von.config.currents[:foo].last.name.must_equal :yearly
39
59
  end
40
60
 
41
61
  end
@@ -4,94 +4,69 @@ describe Von::Counter do
4
4
  Counter = Von::Counter
5
5
 
6
6
  before :each do
7
- Timecop.freeze(Time.local(2013, 01))
7
+ Timecop.freeze(Time.local(2013, 01, 01, 01, 01))
8
8
  Von.config.init!
9
- mock_connection!
9
+ @redis = Redis.new
10
+ @redis.flushall
10
11
  end
11
12
 
12
- it "increments the total counter if given a single key" do
13
- Counter.increment('foo')
14
-
15
- @store.has_key?('von:counters:foo').must_equal true
16
- @store['von:counters:foo']['total'].must_equal 1
17
-
18
- Counter.increment('foo')
19
- @store['von:counters:foo']['total'].must_equal 2
13
+ it "returns count for key" do
14
+ 3.times { Von.increment('foo') }
15
+ Counter.new('foo').total.must_equal 3
20
16
  end
21
17
 
22
- it "increments the total counter for a key and it's parent keys" do
23
- Counter.increment('foo:bar')
24
-
25
- @store.has_key?('von:counters:foo').must_equal true
26
- @store['von:counters:foo']['total'].must_equal 1
27
- @store.has_key?('von:counters:foo:bar').must_equal true
28
- @store['von:counters:foo:bar']['total'].must_equal 1
29
-
30
- Counter.increment('foo:bar')
31
- @store['von:counters:foo']['total'].must_equal 2
32
- @store['von:counters:foo:bar']['total'].must_equal 2
18
+ it "returns count for key and parent keys" do
19
+ 3.times { Von.increment('foo:bar') }
20
+ Counter.new('foo').total.must_equal 3
21
+ Counter.new('foo:bar').total.must_equal 3
33
22
  end
34
23
 
35
- it "increments a month counter" do
24
+
25
+ it "returns counts for a given period" do
36
26
  Von.configure do |config|
37
- config.counter 'foo', :monthly => 1
27
+ config.counter 'foo', :monthly => 2
38
28
  end
39
29
 
40
- Counter.increment('foo')
41
- Counter.increment('foo')
30
+ Von.increment('foo')
31
+ Timecop.freeze(Time.local(2013, 02))
32
+ Von.increment('foo')
33
+ Timecop.freeze(Time.local(2013, 03))
34
+ Von.increment('foo')
42
35
 
43
- @store.has_key?('von:counters:foo').must_equal true
44
- @store.has_key?('von:counters:foo:monthly').must_equal true
45
- @store['von:counters:foo']['total'].must_equal 2
46
- @store['von:counters:foo:monthly']['2013-01'].must_equal 2
47
- @store['von:lists:foo:monthly'].size.must_equal 1
36
+ Counter.new('foo').per(:month).must_equal [{:timestamp => "2013-02", :count => 1}, {:timestamp => "2013-03", :count => 1}]
48
37
  end
49
38
 
50
- it "expires counters past the limit" do
39
+ it "returns best count for a given period" do
51
40
  Von.configure do |config|
52
- config.counter 'foo', :monthly => 1
41
+ config.counter 'foo', :best => [:minute, :week]
53
42
  end
54
43
 
55
- Counter.increment('foo')
56
- Timecop.freeze(Time.local(2013, 02))
57
- Counter.increment('foo')
58
-
59
- @store.has_key?('von:counters:foo').must_equal true
60
- @store.has_key?('von:counters:foo:monthly').must_equal true
61
- @store['von:counters:foo']['total'].must_equal 2
62
- @store['von:counters:foo:monthly'].has_key?('2013-02').must_equal true
63
- @store['von:lists:foo:monthly'].size.must_equal 1
64
- end
44
+ Von.increment('foo')
65
45
 
66
- it "gets a total count for a counter" do
67
- Counter.increment('foo')
68
- Counter.increment('foo')
69
- Counter.increment('foo')
46
+ Timecop.freeze(Time.local(2013, 01, 13, 06, 05))
47
+ 4.times { Von.increment('foo') }
48
+ Timecop.freeze(Time.local(2013, 01, 20, 06, 10))
49
+ 3.times { Von.increment('foo') }
70
50
 
71
- Von.count('foo').must_equal 3
51
+ Counter.new('foo').best(:minute).must_equal({:timestamp => "2013-01-13 06:05", :count => 4})
52
+ Counter.new('foo').best(:week).must_equal({:timestamp => "2013-01-07", :count => 4})
72
53
  end
73
54
 
74
- it "gets a count for a time period and 0s missing entries" do
55
+ it "returns current count for a given period" do
75
56
  Von.configure do |config|
76
- config.counter 'foo', :monthly => 1, :hourly => 6
57
+ config.counter 'foo', :current => [:minute, :day]
77
58
  end
78
59
 
79
- Timecop.freeze(Time.local(2013, 02, 01, 05))
80
- Counter.increment('foo')
81
- Timecop.freeze(Time.local(2013, 02, 01, 07))
82
- Counter.increment('foo')
83
-
84
- Von.count('foo').must_equal 2
85
-
86
- Von.count('foo', :monthly).must_equal [{"2013-02" => 2}]
87
- Von.count('foo', :hourly).must_equal [
88
- {"2013-02-01 02:00"=>0},
89
- {"2013-02-01 03:00"=>0},
90
- {"2013-02-01 04:00"=>0},
91
- {"2013-02-01 05:00"=>1},
92
- {"2013-02-01 06:00"=>0},
93
- {"2013-02-01 07:00"=>1}
94
- ]
60
+ 4.times { Von.increment('foo') }
61
+ Timecop.freeze(Time.local(2013, 01, 20, 06, 10))
62
+ 3.times { Von.increment('foo') }
63
+
64
+ Counter.new('foo').this(:minute).must_equal 3
65
+ Counter.new('foo').current(:minute).must_equal 3
66
+ Counter.new('foo').this(:day).must_equal 3
67
+ Counter.new('foo').current(:day).must_equal 3
68
+ Counter.new('foo').today.must_equal 3
95
69
  end
96
70
 
71
+
97
72
  end