logfile_interval 1.1.1 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/Gemfile.lock +4 -4
- data/README.md +3 -6
- data/docs/design.rb +118 -36
- data/lib/logfile_interval/aggregator/average.rb +14 -0
- data/lib/logfile_interval/aggregator/base.rb +52 -0
- data/lib/logfile_interval/aggregator/count.rb +9 -0
- data/lib/logfile_interval/aggregator/delta.rb +22 -0
- data/lib/logfile_interval/aggregator/group_and_count.rb +14 -0
- data/lib/logfile_interval/aggregator/sum.rb +9 -0
- data/lib/logfile_interval/aggregator.rb +24 -0
- data/lib/logfile_interval/interval.rb +9 -0
- data/lib/logfile_interval/interval_builder.rb +2 -0
- data/lib/logfile_interval/logfile.rb +4 -1
- data/lib/logfile_interval/logfile_set.rb +4 -0
- data/lib/logfile_interval/{line_parser → util}/counter.rb +1 -1
- data/lib/logfile_interval/util/file_backward.rb +51 -0
- data/lib/logfile_interval/version.rb +1 -1
- data/lib/logfile_interval.rb +3 -3
- data/spec/lib/aggregator_spec.rb +208 -0
- data/spec/lib/{line_parser/counter_spec.rb → counter_spec.rb} +1 -1
- data/spec/lib/interval_builder_spec.rb +8 -0
- data/spec/lib/interval_spec.rb +31 -7
- data/spec/lib/logfile_set_spec.rb +39 -14
- data/spec/lib/logfile_spec.rb +43 -18
- metadata +15 -12
- data/docs/design2.rb +0 -77
- data/docs/design3.rb +0 -177
- data/lib/logfile_interval/file_backward.rb +0 -49
- data/lib/logfile_interval/interval_length.rb +0 -47
- data/lib/logfile_interval/line_parser/aggregator.rb +0 -117
- data/spec/lib/line_parser/aggregator_spec.rb +0 -211
data/docs/design3.rb
DELETED
@@ -1,177 +0,0 @@
|
|
1
|
-
module LogfileInterval
|
2
|
-
module LineParser
|
3
|
-
class Base
|
4
|
-
class << self
|
5
|
-
def set_regex(regex)
|
6
|
-
end
|
7
|
-
|
8
|
-
def add_column(name, options)
|
9
|
-
agg = Aggregators.klass(aggregator)
|
10
|
-
@columns[name] = { :pos => pos, :aggregator => agg, :conversion => conversion }
|
11
|
-
define_method(name)
|
12
|
-
end
|
13
|
-
|
14
|
-
def parse(line)
|
15
|
-
match_data = regex.match(line)
|
16
|
-
@data = f(match_data)
|
17
|
-
end
|
18
|
-
|
19
|
-
def create_record(line)
|
20
|
-
record = new(line)
|
21
|
-
return record.valid? ? record : nil
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
class AccessLog < Base
|
28
|
-
set_regex /blah/
|
29
|
-
add_column :name => :foo, :pos => 1, :conversion => integer, :aggregator => :average
|
30
|
-
|
31
|
-
def initialize(line)
|
32
|
-
@data = self.class.parse(line)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
module Aggregator
|
37
|
-
def self.klass(aggregator)
|
38
|
-
case aggregator
|
39
|
-
when :sum then Sum
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
class Sum
|
44
|
-
def initialize
|
45
|
-
@val = 0
|
46
|
-
end
|
47
|
-
|
48
|
-
def add(value)
|
49
|
-
@val += value
|
50
|
-
end
|
51
|
-
|
52
|
-
def value
|
53
|
-
@val
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
class Count
|
58
|
-
def initialize
|
59
|
-
@val = Counter.new
|
60
|
-
end
|
61
|
-
|
62
|
-
def add(value)
|
63
|
-
@val.increment(value)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
class Logfile
|
70
|
-
def initialize(filename, parser)
|
71
|
-
end
|
72
|
-
|
73
|
-
def each_line
|
74
|
-
end
|
75
|
-
|
76
|
-
def each_parsed_line
|
77
|
-
each_line do |line|
|
78
|
-
record = parser.create_record(line)
|
79
|
-
yield record if record
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
class LogfileSet
|
85
|
-
def initialize(filenames_array, parser)
|
86
|
-
end
|
87
|
-
|
88
|
-
def ordered_filenams
|
89
|
-
end
|
90
|
-
|
91
|
-
def each_line
|
92
|
-
end
|
93
|
-
|
94
|
-
def each_parsed_line
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
class IntervalBuilder
|
99
|
-
def initialize(logfile_set, length)
|
100
|
-
parser = logfile_set.parser
|
101
|
-
end
|
102
|
-
|
103
|
-
def each_interval
|
104
|
-
interval = Interval.new(now, length)
|
105
|
-
set.each_parsed_line(parser) do |record|
|
106
|
-
while record.time < interval.start_time do
|
107
|
-
yield interval
|
108
|
-
interval = Interval.new(interval.start_time, length)
|
109
|
-
end
|
110
|
-
interval.add(record)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
class Counter < Hash
|
116
|
-
def increment(key)
|
117
|
-
self[key] = self[key] ? self[key] + 1 : 1
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
class Interval
|
122
|
-
def initialize(end_time, length, parser)
|
123
|
-
@data = {}
|
124
|
-
parser.columns.each do |name, options|
|
125
|
-
@data[name] = options[:aggregator].new
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def [](name)
|
130
|
-
@data[name].value
|
131
|
-
end
|
132
|
-
|
133
|
-
def add_record(record)
|
134
|
-
return unless record.valid?
|
135
|
-
raise ParserMismatch unless record.class == parser
|
136
|
-
|
137
|
-
@size += 1
|
138
|
-
parser.columns.each do |name, options|
|
139
|
-
@data[name].add(record[name])
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
logfiles = [ 'access.log', 'access.log.1', 'access.log.2' ]
|
146
|
-
logfile = logfiles.first
|
147
|
-
|
148
|
-
parser = LineParser::AccessLog
|
149
|
-
|
150
|
-
logfile_iterator = LogfileInterval::Logfile.new(logfile, parser)
|
151
|
-
logfile_iterator.each_line do |line|
|
152
|
-
puts line.class # String
|
153
|
-
puts line
|
154
|
-
end
|
155
|
-
|
156
|
-
parser = LineParser::AccessLog
|
157
|
-
logfile_iterator.each_parsed_line do |record|
|
158
|
-
puts record.class # LineParser::AccessLog
|
159
|
-
puts record.ip
|
160
|
-
puts record.time
|
161
|
-
end
|
162
|
-
|
163
|
-
set_iterator = LogfileInterval::LogfileSet.new(logfiles, parser)
|
164
|
-
set_iterator.each_parsed_line do |record|
|
165
|
-
puts record.class # LineParser::AccessLog
|
166
|
-
end
|
167
|
-
|
168
|
-
length = 5.minutes
|
169
|
-
interval_builder = LogfileInterval::IntervalBuilder.new(logfiles, length)
|
170
|
-
interval_builder.each_interval do |interval|
|
171
|
-
puts interval.class # LogfileInterval::Interval
|
172
|
-
puts interval.start_time
|
173
|
-
puts interval.length
|
174
|
-
interval[:ip].each do |ip, count|
|
175
|
-
puts "#{ip}, #{count}"
|
176
|
-
end
|
177
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module LogfileInterval
|
2
|
-
# Based on Perl's File::ReadBackwards module, by Uri Guttman.
|
3
|
-
class FileBackward
|
4
|
-
MAX_READ_SIZE = 1 << 10 # 1024
|
5
|
-
|
6
|
-
def initialize( *args )
|
7
|
-
return unless File.exist?(args[0])
|
8
|
-
@file = File.new(*args)
|
9
|
-
@file.seek(0, IO::SEEK_END)
|
10
|
-
|
11
|
-
@current_pos = @file.pos
|
12
|
-
|
13
|
-
@read_size = @file.pos % MAX_READ_SIZE
|
14
|
-
@read_size = MAX_READ_SIZE if @read_size.zero?
|
15
|
-
|
16
|
-
@line_buffer = Array.new
|
17
|
-
end
|
18
|
-
|
19
|
-
def gets( sep_string = $/ )
|
20
|
-
return nil unless @file
|
21
|
-
return @line_buffer.pop if @line_buffer.size > 2 or @current_pos.zero?
|
22
|
-
|
23
|
-
@current_pos -= @read_size
|
24
|
-
@file.seek(@current_pos, IO::SEEK_SET)
|
25
|
-
|
26
|
-
@line_buffer[0] = "#{@file.read(@read_size)}#{@line_buffer[0]}"
|
27
|
-
@read_size = MAX_READ_SIZE # Set a size for the next read.
|
28
|
-
|
29
|
-
@line_buffer[0] =
|
30
|
-
@line_buffer[0].scan(/.*?#{Regexp.escape(sep_string)}|.+/)
|
31
|
-
@line_buffer.flatten!
|
32
|
-
|
33
|
-
gets(sep_string)
|
34
|
-
end
|
35
|
-
|
36
|
-
def close
|
37
|
-
return unless @file
|
38
|
-
@file.close()
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# f = FileBackward.new('../log/development.log')
|
44
|
-
# i = 0
|
45
|
-
# while(line = f.gets())
|
46
|
-
# puts line
|
47
|
-
# i += 1
|
48
|
-
# break if i>30
|
49
|
-
# end
|
@@ -1,47 +0,0 @@
|
|
1
|
-
module LogfileInterval
|
2
|
-
class IntervalLength
|
3
|
-
MAX_PERIODS = { 5 * 60 => 6 * 3600,
|
4
|
-
3600 => 3600 * 24,
|
5
|
-
3600 * 24 => 3600 * 24 * 30,
|
6
|
-
3600 * 24 * 30 => 365 * 3600 * 24 }
|
7
|
-
LENGTHS = MAX_PERIODS.keys.sort
|
8
|
-
|
9
|
-
attr_reader :length
|
10
|
-
|
11
|
-
def initialize(l)
|
12
|
-
raise ArgumentError unless LENGTHS.include?(l)
|
13
|
-
@length = l
|
14
|
-
end
|
15
|
-
|
16
|
-
def lower
|
17
|
-
pos = LENGTHS.index(@length)
|
18
|
-
return nil if pos==0
|
19
|
-
IntervalLength.new(LENGTHS[pos-1])
|
20
|
-
end
|
21
|
-
|
22
|
-
def higher
|
23
|
-
pos = LENGTHS.index(@length)
|
24
|
-
return nil if pos==LENGTHS.size-1
|
25
|
-
IntervalLength.new(LENGTHS[pos+1])
|
26
|
-
end
|
27
|
-
|
28
|
-
def smallest?
|
29
|
-
pos = LENGTHS.index(@length)
|
30
|
-
pos == 0
|
31
|
-
end
|
32
|
-
|
33
|
-
def start_time(t)
|
34
|
-
ts = (t.to_i / @length.to_i) * @length.to_i
|
35
|
-
ts -= @length.to_i if t.to_i % @length.to_i == 0
|
36
|
-
Time.at(ts)
|
37
|
-
end
|
38
|
-
|
39
|
-
def end_time(t)
|
40
|
-
start_time(t) + @length
|
41
|
-
end
|
42
|
-
|
43
|
-
def to_i
|
44
|
-
@length.to_i
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,117 +0,0 @@
|
|
1
|
-
module LogfileInterval
|
2
|
-
module LineParser
|
3
|
-
module Aggregator
|
4
|
-
def self.klass(aggregator)
|
5
|
-
case aggregator
|
6
|
-
when :sum then Sum
|
7
|
-
when :average then Average
|
8
|
-
when :count then Count
|
9
|
-
when :group_and_count then GroupAndCount
|
10
|
-
when :delta then Delta
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
class Base
|
15
|
-
include Enumerable
|
16
|
-
|
17
|
-
def initialize
|
18
|
-
@val = Counter.new
|
19
|
-
@size = Counter.new
|
20
|
-
end
|
21
|
-
|
22
|
-
def value(group = nil)
|
23
|
-
val(key(group))
|
24
|
-
end
|
25
|
-
|
26
|
-
def values
|
27
|
-
if single_value?
|
28
|
-
value
|
29
|
-
else
|
30
|
-
self.inject({}) { |h, v| h[v[0]] = v[1]; h }
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def add(value, group_by = nil)
|
35
|
-
raise NotImplementedError
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
def key(group_by = nil)
|
40
|
-
group_by ? group_by : :all
|
41
|
-
end
|
42
|
-
|
43
|
-
def single_value?
|
44
|
-
return true if @val.empty?
|
45
|
-
@val.keys.count == 1 && @val.keys.first == :all
|
46
|
-
end
|
47
|
-
|
48
|
-
def each
|
49
|
-
@val.each_key do |k|
|
50
|
-
yield k, val(k)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def val(k)
|
55
|
-
@val[k]
|
56
|
-
end
|
57
|
-
|
58
|
-
def average(k)
|
59
|
-
@size[k] > 0 ? @val[k].to_f / @size[k].to_f : 0
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
class Sum < Base
|
64
|
-
def add(value, group_by = nil)
|
65
|
-
@val.add(key(group_by), value)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
class Average < Base
|
70
|
-
def add(value, group_by = nil)
|
71
|
-
@val.add(key(group_by), value)
|
72
|
-
@size.increment(key(group_by))
|
73
|
-
end
|
74
|
-
|
75
|
-
def val(k)
|
76
|
-
average(k)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
class Count < Base
|
81
|
-
def add(value, group_by = nil)
|
82
|
-
@val.add(key(group_by), 1)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
class GroupAndCount < Base
|
87
|
-
def each
|
88
|
-
@val.each { |k, v| yield k, v }
|
89
|
-
end
|
90
|
-
|
91
|
-
def add(value, group_by)
|
92
|
-
raise ArgumentError, 'group_by argument is mandatory for GroupAndCount#add' unless group_by
|
93
|
-
@val.increment_subkey(value, key(group_by))
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
class Delta < Base
|
98
|
-
def initialize
|
99
|
-
@previous = Counter.new
|
100
|
-
super
|
101
|
-
end
|
102
|
-
|
103
|
-
def add(value, group_by = nil)
|
104
|
-
if @previous.has_key?(key(group_by))
|
105
|
-
@val.add(key(group_by), @previous[key(group_by)] - value)
|
106
|
-
@size.increment(key(group_by))
|
107
|
-
end
|
108
|
-
@previous.set(key(group_by), value)
|
109
|
-
end
|
110
|
-
|
111
|
-
def val(k)
|
112
|
-
average(k)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
@@ -1,211 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module LogfileInterval
|
4
|
-
module LineParser
|
5
|
-
module Aggregator
|
6
|
-
describe Aggregator do
|
7
|
-
it 'finds the aggregator class' do
|
8
|
-
Aggregator.klass(:sum).should == Sum
|
9
|
-
Aggregator.klass(:average).should == Average
|
10
|
-
Aggregator.klass(:count).should == Count
|
11
|
-
Aggregator.klass(:group_and_count).should == GroupAndCount
|
12
|
-
Aggregator.klass(:delta).should == Delta
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
shared_examples 'an aggregator' do
|
17
|
-
let(:aggregator) { described_class.new }
|
18
|
-
|
19
|
-
[ :add, :value, :values ].each do |method|
|
20
|
-
it "responds to #{method}" do
|
21
|
-
aggregator.should respond_to(method)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
context 'values' do
|
26
|
-
context 'with one group' do
|
27
|
-
before :each do
|
28
|
-
aggregator.add(5, :key1)
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'returns a hash' do
|
32
|
-
aggregator.values.should be_a(Hash) unless aggregator.is_a?(Delta)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
context 'with several groups' do
|
37
|
-
before :each do
|
38
|
-
aggregator.add(5, :key1)
|
39
|
-
aggregator.add(3, :key2)
|
40
|
-
aggregator.add(3, :key1)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'returns a hash' do
|
44
|
-
aggregator.values.should be_a(Hash)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
context 'with no group' do
|
49
|
-
before :each do
|
50
|
-
aggregator.add(5)
|
51
|
-
aggregator.add(3)
|
52
|
-
end
|
53
|
-
|
54
|
-
it 'returns a numeric' do
|
55
|
-
aggregator.values.should be_a(Numeric) unless aggregator.is_a?(Count)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
[ Count, Sum, Average, Delta ]. each do |klass|
|
62
|
-
describe klass do
|
63
|
-
it_behaves_like 'an aggregator'
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
describe 'without group_by key' do
|
69
|
-
describe Sum do
|
70
|
-
it 'sums up values' do
|
71
|
-
sum = Sum.new
|
72
|
-
sum.add(3)
|
73
|
-
sum.add(5)
|
74
|
-
sum.value.should == 8
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
describe Average do
|
79
|
-
it 'averages values' do
|
80
|
-
avg = Average.new
|
81
|
-
avg.add(3)
|
82
|
-
avg.add(5)
|
83
|
-
avg.value.should == 4
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
describe Delta do
|
88
|
-
it 'averages delta values' do
|
89
|
-
d = Delta.new
|
90
|
-
d.add(1.4)
|
91
|
-
d.add(1.1)
|
92
|
-
d.add(1.0)
|
93
|
-
d.value.round(5).should == 0.2
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
describe Count do
|
98
|
-
it 'groups values and increment counters' do
|
99
|
-
g = Count.new
|
100
|
-
g.add('200')
|
101
|
-
g.add('500')
|
102
|
-
g.add('301')
|
103
|
-
g.add('200')
|
104
|
-
g.value.should == 4
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
describe 'with group_by key' do
|
110
|
-
|
111
|
-
describe Sum do
|
112
|
-
it 'sums up values by key' do
|
113
|
-
sum = Sum.new
|
114
|
-
sum.add(3, :key1)
|
115
|
-
sum.add(5, :key2)
|
116
|
-
sum.add(5, :key1)
|
117
|
-
sum.values.should be_a(Hash)
|
118
|
-
sum.values.size.should == 2
|
119
|
-
sum.value(:key1).should == 8
|
120
|
-
sum.values[:key1].should == 8
|
121
|
-
sum.value(:key2).should == 5
|
122
|
-
sum.values[:key2].should == 5
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
|
127
|
-
describe Average do
|
128
|
-
it 'averages values by key' do
|
129
|
-
avg = Average.new
|
130
|
-
avg.add(3, :key1)
|
131
|
-
avg.add(5, :key2)
|
132
|
-
avg.add(5, :key1)
|
133
|
-
avg.values.should be_a(Hash)
|
134
|
-
avg.values.size.should == 2
|
135
|
-
avg.value(:key1).should == 4
|
136
|
-
avg.values[:key1].should == 4
|
137
|
-
avg.value(:key2).should == 5
|
138
|
-
avg.values[:key2].should == 5
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
describe Count do
|
143
|
-
it 'groups values and increment counters' do
|
144
|
-
g = Count.new
|
145
|
-
g.add('200', '200')
|
146
|
-
g.add('500', '500')
|
147
|
-
g.add('301', '301')
|
148
|
-
g.add('200', '200')
|
149
|
-
g.values.should be_a(Hash)
|
150
|
-
g.values.should include({'200' => 2})
|
151
|
-
g.values.should include({'301' => 1})
|
152
|
-
g.values.should include({'500' => 1})
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
describe GroupAndCount do
|
157
|
-
it 'each yields a key and a hash' do
|
158
|
-
gac = GroupAndCount.new
|
159
|
-
gac.add :key1, :subkey1
|
160
|
-
gac.first.should be_an(Array)
|
161
|
-
gac.first.size.should == 2
|
162
|
-
gac.first[1].should be_a(Hash)
|
163
|
-
end
|
164
|
-
|
165
|
-
context :add do
|
166
|
-
before :each do
|
167
|
-
@gac = GroupAndCount.new
|
168
|
-
end
|
169
|
-
|
170
|
-
it 'requires a group_by argument' do
|
171
|
-
lambda { @gac.add('foo') }.should raise_error ArgumentError
|
172
|
-
end
|
173
|
-
|
174
|
-
it 'counts number of occurence of subkey for key' do
|
175
|
-
@gac.add :key1, :subkey1
|
176
|
-
@gac.add :key1, :subkey2
|
177
|
-
@gac.add :key2, :subkey1
|
178
|
-
@gac.add :key2, :subkey1
|
179
|
-
@gac.add :key2, :subkey3
|
180
|
-
|
181
|
-
@gac.values[:key1][:subkey1].should == 1
|
182
|
-
@gac.values[:key1][:subkey2].should == 1
|
183
|
-
@gac.values[:key2][:subkey1].should == 2
|
184
|
-
@gac.values[:key2][:subkey2].should == 0
|
185
|
-
@gac.values[:key2][:subkey3].should == 1
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
describe Delta do
|
191
|
-
it 'averages deltas by key' do
|
192
|
-
d = Delta.new
|
193
|
-
d.add(9, :key1)
|
194
|
-
d.add(10, :key2)
|
195
|
-
d.add(5, :key1)
|
196
|
-
d.add(8, :key2)
|
197
|
-
d.add(3, :key1)
|
198
|
-
d.add(5, :key2)
|
199
|
-
d.values.should be_a(Hash)
|
200
|
-
d.values.size.should == 2
|
201
|
-
d.value(:key1).should == 3
|
202
|
-
d.values[:key1].should == 3
|
203
|
-
d.value(:key2).should == 2.5
|
204
|
-
d.values[:key2].should == 2.5
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
end
|