omega-tariffs-base 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1 @@
1
+ v0.1. Initial code commit. Everything is working and tested.
data/Manifest ADDED
@@ -0,0 +1,30 @@
1
+ CHANGELOG
2
+ Manifest
3
+ README.rdoc
4
+ Rakefile
5
+ lib/computer_class_filter.rb
6
+ lib/deny_filter.rb
7
+ lib/filter.rb
8
+ lib/omega-tariffs-base.rb
9
+ lib/packet_filter.rb
10
+ lib/rate_filter.rb
11
+ lib/regioned_calculation.rb
12
+ lib/repeating_filter.rb
13
+ lib/settings.rb
14
+ lib/single_time_filter.rb
15
+ lib/tarif_builder.rb
16
+ lib/weekday_filter.rb
17
+ spec/complex_tarif_spec.rb
18
+ spec/computer_class_filter_spec.rb
19
+ spec/deny_filter_spec.rb
20
+ spec/filter_spec.rb
21
+ spec/lib/active_record_classes_stub.rb
22
+ spec/lib/init.rb
23
+ spec/lib/spec_helper.rb
24
+ spec/packet_filter_spec.rb
25
+ spec/rate_filter_spec.rb
26
+ spec/repeating_filter_spec.rb
27
+ spec/settings_spec.rb
28
+ spec/single_time_filter_spec.rb
29
+ spec/tarif_builder_spec.rb
30
+ spec/weekday_filter_spec.rb
data/README.rdoc ADDED
@@ -0,0 +1,10 @@
1
+ = Uniq Systems Omega Sector Generic Tariffs Gem
2
+
3
+ This gem provides basic code to support Omega Sector tariffs. In order for them to work you've got to redefine
4
+ several classes to support real persistent database storage instead of the in-memory one.
5
+
6
+ These classes are:
7
+ * PacketFilterRepository
8
+ * SingleTimeFilterRepository
9
+
10
+ Without those every time you restart the system you'll lose the state, which is probably not what you really want.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('omega-tariffs-base', '0.1.5') do |p|
6
+ p.description = "Omega Sector base tariffs"
7
+ p.url = "http://uniqsys.ru/"
8
+ p.author = "Uniq Systems"
9
+ p.email = "ivan@uniqsystems.ru"
10
+ p.ignore_pattern = ["tmp/*", "script/*"]
11
+ p.runtime_dependencies = p.development_dependencies = ["activesupport =2.3.8"]
12
+ end
13
+
14
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
@@ -0,0 +1,37 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class ComputerClassFilter < Filter
5
+ def start_permitted?(computer_id, login_id, options={})
6
+ computer_class(computer_id).start_permitted?(computer_id, login_id, options)
7
+ end
8
+
9
+ def permitted?(computer_id, login_id, options={})
10
+ computer_class(computer_id).permitted?(computer_id, login_id, options)
11
+ end
12
+
13
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
14
+ computer_class(computer_id).calculate_cost(computer_id, login_id, started_at, amount, options)
15
+ end
16
+
17
+ def process_activity(unprocessed_activity, ft, options={})
18
+ computer_class(unprocessed_activity.computer_id).process_activity(unprocessed_activity, ft, options)
19
+ end
20
+
21
+ protected
22
+ def computer_class(computer_id)
23
+ @classes[Computer.find(computer_id).computer_class_id]
24
+ end
25
+
26
+ def after_initialize
27
+ raise "Some classes left undefined." unless ComputerClass.all.map { |klass| klass.id }.to_a.sort == settings.to_hash.keys.sort
28
+ @classes = [ ]
29
+ settings.to_hash.each do |computer_class_id, settings|
30
+ @classes[computer_class_id] = TarifBuilder.build Settings.new(settings), tarif_id
31
+ end
32
+ @classes.compact.size == ComputerClass.count
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class DenyFilter < Filter
5
+ def start_permitted?(computer_id, login_id, options={})
6
+ false
7
+ end
8
+
9
+ def permitted?(computer_id, login_id, options={})
10
+ false
11
+ end
12
+
13
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
14
+ Integer::MAX
15
+ end
16
+
17
+ def process_activity(unprocessed_activity, ft, options={})
18
+ # TODO: add logging, so it doesn't just silently swallow residues of the activities
19
+ true
20
+ end
21
+ end
22
+
23
+ end
24
+ end
data/lib/filter.rb ADDED
@@ -0,0 +1,43 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class Filter
5
+ def initialize(settings, tarif_id)
6
+ @tarif_id = tarif_id
7
+ @settings = settings
8
+ after_initialize
9
+ end
10
+
11
+ def start_permitted?(computer_id, login_id, options={})
12
+ raise "Unimplemented"
13
+ end
14
+
15
+ def permitted?(computer_id, login_id, options={})
16
+ raise "Unimplemented"
17
+ end
18
+
19
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
20
+ raise "Unimplemented"
21
+ end
22
+
23
+ def process_activity(unprocessed_activity, ft, options={})
24
+ raise "Unimplemented"
25
+ end
26
+
27
+ protected
28
+ attr_accessor :tarif_id
29
+ attr_accessor :settings
30
+
31
+ def after_initialize
32
+ end
33
+
34
+ def alter_activity_period(unprocessed_activity, new_starts_at, new_amount)
35
+ unprocessed_activity.dup.tap do |activity|
36
+ activity.created_at = new_starts_at
37
+ activity.amount = new_amount
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # guerilla patch the Integer class
2
+ class Integer
3
+ N_BYTES = [42].pack('i').size
4
+ N_BITS = N_BYTES * 8
5
+ MAX = 2 ** (N_BITS - 2) - 1
6
+ MIN = -MAX - 1
7
+ end
8
+
9
+ # load libraries
10
+ [ :settings, :regioned_calculation, :tarif_builder, :filter, :deny_filter, :weekday_filter,
11
+ :rate_filter, :repeating_filter, :single_time_filter, :computer_class_filter, :packet_filter ].each do |lib|
12
+ require File.join(File.dirname(__FILE__), lib.to_s)
13
+ end
@@ -0,0 +1,111 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class PacketFilterRepository
5
+ class <<self
6
+ def register_region(tarif_id, login_id, moment, period_duration)
7
+ @payments ||= { }
8
+ @payments[[tarif_id, login_id]] ||= [ ]
9
+ @payments[[tarif_id, login_id]] << { :period => (moment ... moment + period_duration), :paid => false }
10
+ end
11
+
12
+ def set_should_end(tarif_id, login_id, time)
13
+ @end_at ||= { }
14
+ @end_at[[tarif_id, login_id]] = time
15
+ true
16
+ end
17
+
18
+ def should_end?(tarif_id, login_id)
19
+ return false if @end_at[[tarif_id, login_id]].nil?
20
+ Time.now > @end_at[[tarif_id, login_id]]
21
+ end
22
+
23
+ def pay_region(tarif_id, login_id, time=Time.now)
24
+ region = find_region(tarif_id, login_id, time)
25
+ return false unless region
26
+ return true if region[:paid]
27
+ region[:paid] = true
28
+ end
29
+
30
+ def region_registered?(*args)
31
+ !find_region(*args).nil?
32
+ end
33
+
34
+ def region_spans?(tarif_id, login_id, moment=Time.now, duration=1)
35
+ region = find_region(tarif_id, login_id, moment)
36
+ return false if region.nil?
37
+ !(region[:period] === (moment + duration))
38
+ end
39
+
40
+ def region_paid?(tarif_id, login_id, moment=Time.now, duration=1)
41
+ region = find_region(tarif_id, login_id, moment)
42
+ !region.nil? && region[:period] === (moment + duration) && region[:paid]
43
+ end
44
+
45
+ def reset
46
+ @payments = { }
47
+ @end_at = { }
48
+ end
49
+
50
+ protected
51
+ def find_region(tarif_id, login_id, time=Time.now)
52
+ return nil unless @payments
53
+ return nil unless @payments[[tarif_id, login_id]]
54
+ @payments[[tarif_id, login_id]].find { |r| r[:period] === time }
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ class PacketFilter < Filter
61
+ def start_permitted?(computer_id, login_id, options={})
62
+ single_payment_unneeded?(login_id) and PacketFilterRepository.set_should_end(tarif_id, login_id, nil)
63
+ end
64
+
65
+ def permitted?(computer_id, login_id, options={})
66
+ return false if PacketFilterRepository.should_end?(tarif_id, login_id)
67
+ single_payment_unneeded? login_id
68
+ end
69
+
70
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
71
+ return Integer::MAX if PacketFilterRepository.region_spans?(tarif_id, login_id, started_at, amount)
72
+ PacketFilterRepository.region_paid?(tarif_id, login_id, started_at) ?
73
+ 0 : @price
74
+ end
75
+
76
+ def process_activity(unprocessed_activity, ft, options={})
77
+ if PacketFilterRepository.region_paid?(tarif_id, unprocessed_activity.login_id, unprocessed_activity.created_at)
78
+ true
79
+ else
80
+ make_single_payment(unprocessed_activity.created_at, ft, unprocessed_activity.login_id) and
81
+ PacketFilterRepository.set_should_end(tarif_id, unprocessed_activity.login_id, unprocessed_activity.created_at+@length)
82
+ end
83
+ end
84
+
85
+ protected
86
+ def single_payment_unneeded?(login_id)
87
+ PacketFilterRepository.region_paid?(tarif_id, login_id) || can_make_single_payment?(login_id)
88
+ end
89
+
90
+ def can_make_single_payment?(login_id)
91
+ Login.find(login_id).balance - @price >= 0
92
+ end
93
+
94
+ def make_single_payment(time, ft, login_id)
95
+ return false unless can_make_single_payment?(login_id)
96
+
97
+ PacketFilterRepository.register_region(tarif_id, login_id, time, @length)
98
+
99
+ ft = ft.dup
100
+ ft.volume = @price
101
+ ft.save and PacketFilterRepository.pay_region(tarif_id, login_id, time)
102
+ end
103
+
104
+ def after_initialize
105
+ @price = settings.price
106
+ @length = settings.length
107
+ end
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,31 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+
4
+ class RateFilter < Filter
5
+ def start_permitted?(computer_id, login_id, options={})
6
+ true
7
+ end
8
+
9
+ def permitted?(computer_id, login_id, options={})
10
+ true
11
+ end
12
+
13
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
14
+ amount/3600*@rate
15
+ end
16
+
17
+ def process_activity(unprocessed_activity, ft, options={})
18
+ ft = ft.dup
19
+ ft.volume = unprocessed_activity.amount/3600*@rate
20
+ ft.save!
21
+ true
22
+ end
23
+
24
+ protected
25
+ def after_initialize
26
+ @rate = settings.rate
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,61 @@
1
+ module Math
2
+ # optimized version of find_root, performs 10000 roots' calculations per sec on Core 2 Duo 2.67
3
+ def self.find_root(a, b, precision=0.01, allow_oob_roots=false, &blk)
4
+ fa, fb = yield(a).to_f, yield(b).to_f
5
+ sa, sb = fa >= 0 ? 1 : -1, fb >= 0 ? 1 : -1
6
+ while true do
7
+ # puts "f(#{a},#{(a/precision).truncate})=#{fa}, f(#{b},#{(b/precision).truncate})=#{fb}"
8
+ raise "Looking for roots out of bounds" if !allow_oob_roots && sa == sb
9
+ return a if (a-b).abs < precision || fa.abs <= precision
10
+ return b if fb.abs <= precision
11
+ c = (a.to_f + b.to_f) / 2
12
+ fc = yield(c)
13
+ sc = fc >= 0 ? 1 : -1
14
+ if sa != sc
15
+ b, fb, sb = c, fc, sc
16
+ else
17
+ a, fa, sa = c, fc, sc
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ class Range
24
+ def intersection(other)
25
+ raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
26
+ raise ArgumentError, 'must be the same end inclusive/non-inclusive type' unless exclude_end? == other.exclude_end?
27
+
28
+ min, max = first, last
29
+ other_min, other_max = other.first, other.last
30
+
31
+ new_min = self === other_min ? other_min : other === min ? min : min == other_min ? min : nil
32
+ new_max = self === other_max ? other_max : other === max ? max : max == other_max ? max : nil
33
+
34
+ new_min && new_max ? (exclude_end? ? new_min...new_max : new_min..new_max) : nil
35
+ end
36
+
37
+ def subtract(range)
38
+ return [ self ] unless range
39
+ raise ArgumentError, 'must be the same end inclusive/non-inclusive type' unless exclude_end? == range.exclude_end?
40
+
41
+ common = self & range
42
+ return [ self ] unless common
43
+
44
+ # отрезок вычли полностью
45
+ return [ ] if common == self
46
+
47
+ if self.end == common.end
48
+ return [ Range.new( self.begin, common.begin, exclude_end? ) ]
49
+ end
50
+
51
+ if self.begin == common.begin
52
+ return [ Range.new( common.end, self.end, exclude_end? ) ]
53
+ end
54
+
55
+ [ Range.new( self.begin, common.begin, exclude_end? ),
56
+ Range.new( common.end, self.end, exclude_end? ) ]
57
+ end
58
+
59
+ alias_method :&, :intersection
60
+ alias_method :-, :subtract
61
+ end
@@ -0,0 +1,188 @@
1
+ module UniqSysOmega
2
+ module Tariffs
3
+ require 'mathn'
4
+
5
+ class SortableRange < Range
6
+ attr_accessor :filter
7
+
8
+ def <=>(other)
9
+ self.begin <=> other.begin
10
+ end
11
+
12
+ def as_range
13
+ Range.new(self.begin, self.end, self.exclude_end?)
14
+ end
15
+ end
16
+
17
+ class RepeatingFilterPeriod
18
+ attr_accessor :starts_at
19
+ attr_accessor :length
20
+ attr_accessor :period
21
+ attr_accessor :filter
22
+
23
+ def initialize(starts_at, length, period, filter)
24
+ raise "period should be more than length, but this assertion is not met: #{length} <= #{period}" if length > period
25
+ @starts_at = starts_at.to_i
26
+ @length = length
27
+ @period = period
28
+ @filter = filter
29
+ end
30
+
31
+ def self.from_hash(hash, filter=nil)
32
+ self.new(hash[:starts_at], hash[:length], hash[:period], filter || hash[:filter])
33
+ end
34
+
35
+ def ===(value)
36
+ includes?(value)
37
+ end
38
+
39
+ def local_period(value)
40
+ local_period_start = @starts_at + ((value.to_i-@starts_at)/@period).floor * @period
41
+ SortableRange.new(local_period_start, local_period_start+@length, true).tap do |range|
42
+ range.filter = filter
43
+ end
44
+ end
45
+
46
+ def includes?(value)
47
+ local_period(value).include?(value.to_i)
48
+ end
49
+
50
+ def generate_in_range(start, finish)
51
+ start, finish = start.to_i, finish.to_i
52
+ [ ].tap do |ranges|
53
+ local_period_start = @starts_at + ((start-@starts_at).to_f/@period.to_f).ceil * @period
54
+ while local_period_start+@length <= finish
55
+ ranges << SortableRange.new(local_period_start, local_period_start+@length, true).tap { |r| r.filter = filter }
56
+ local_period_start += @period
57
+ end
58
+ end
59
+ end
60
+
61
+ def to_s
62
+ "RepeatingFilterPeriod(starts_at=#{starts_at}, length=#{length}, period=#{period})"
63
+ end
64
+ end
65
+
66
+ module RepeatingFilterPeriodsVerification
67
+ private
68
+ def fail_because_of_gaps!
69
+ periods_text = @periods.map { |p| " - starting: #{p.starts_at}, length: #{p.length}, period: #{p.period}" }.join("\n")
70
+ gaps_text = gapped_periods.map { |g| " - starting: #{g.begin}, length: #{g.end-g.begin}" }.join("\n")
71
+ raise "Inconsistent periods:\n#{periods_text}\n\nGaps:\n#{gaps_text}\n"
72
+ end
73
+
74
+ def fail_because_of_intersections!
75
+ periods_text = intersecting_periods.map { |p| " - #{p[0]} and #{p[1]}" }.join("\n")
76
+ raise "Intersectings periods:\n#{periods_text}"
77
+ end
78
+
79
+ def total_period_start
80
+ @periods.map { |p| p.starts_at }.min
81
+ end
82
+
83
+ def total_period_length
84
+ max_period_length = @periods.map { |p| p.starts_at + p.length }.max - total_period_start
85
+ [ @periods.map { |p| p.period }.inject(&:lcm), max_period_length ].max
86
+ end
87
+
88
+ def repeating_periods_chunks
89
+ @periods.map { |period| period.generate_in_range(total_period_start, total_period_start + total_period_length) }.flatten
90
+ end
91
+
92
+ def gapped_periods
93
+ @gapped_periods = @gapped_periods || [ (total_period_start ... total_period_start + total_period_length) ].tap do |uncovered_periods|
94
+ repeating_periods_chunks.each do |period|
95
+ uncovered_periods.map! { |uncovered_period| uncovered_period - (uncovered_period & period) }
96
+ uncovered_periods.flatten!
97
+ end
98
+ end
99
+ end
100
+
101
+ def periods_consistent?
102
+ gapped_periods.size == 0
103
+ end
104
+
105
+ def intersecting_periods
106
+ @intersecting_periods = @intersecting_periods || [].tap do |intersecting|
107
+ chunks = repeating_periods_chunks.sort
108
+ while chunks.size >= 2
109
+ intersecting << [chunks[0], chunks[1]] unless (chunks[0] & chunks[1]).nil?
110
+ chunks.shift
111
+ end
112
+ end
113
+ end
114
+
115
+ def periods_not_intersecting?
116
+ intersecting_periods.size == 0
117
+ end
118
+
119
+ def ensure_periods_are_correct!
120
+ fail_because_of_intersections! unless periods_not_intersecting?
121
+ fail_because_of_gaps! unless periods_consistent?
122
+ end
123
+ end
124
+
125
+ class RepeatingFilter < Filter
126
+ def start_permitted?(computer_id, login_id, options={})
127
+ period = period_matching_time(Time.now)
128
+ period.filter.start_permitted?(computer_id, login_id, options.merge({ :period => period.as_range }))
129
+ end
130
+
131
+ def permitted?(computer_id, login_id, options={})
132
+ period = period_matching_time(Time.now)
133
+ period.filter.permitted?(computer_id, login_id, options.merge({ :period => period.as_range }))
134
+ end
135
+
136
+ def calculate_cost(computer_id, login_id, started_at, amount, options={})
137
+ actual_periods(started_at, amount).inject(0) do |overall, p|
138
+ overall += p[:actual].filter.calculate_cost(computer_id, login_id, p[:actual].begin, p[:actual].end-p[:actual].begin, options.merge({ :period => p[:covering].as_range }))
139
+ end
140
+ end
141
+
142
+ def process_activity(unprocessed_activity, ft, options={})
143
+ actual_periods(unprocessed_activity.created_at, unprocessed_activity.amount).each do |p|
144
+ p[:actual].filter.process_activity(
145
+ alter_activity_period(unprocessed_activity, p[:actual].begin, p[:actual].end-p[:actual].begin),
146
+ ft,
147
+ options.merge({ :period => p[:covering].as_range})) or return false
148
+ end
149
+ true
150
+ end
151
+
152
+ protected
153
+ include RepeatingFilterPeriodsVerification
154
+
155
+ def after_initialize
156
+ @periods = [ ]
157
+ settings.to_hash.each do |period, settings|
158
+ @periods << RepeatingFilterPeriod.from_hash(period, TarifBuilder.build(Settings.new(settings), tarif_id))
159
+ end
160
+ ensure_periods_are_correct!
161
+ end
162
+
163
+ def actual_periods(started_at, amount)
164
+ covering_periods(started_at, started_at+amount).map do |period|
165
+ actual_period = ( period.begin ... period.end ) & ( started_at.to_i ... started_at.to_i+amount )
166
+ {
167
+ :covering => period,
168
+ :actual =>
169
+ SortableRange.new(Time.at(actual_period.begin),
170
+ Time.at(actual_period.end),
171
+ actual_period.exclude_end?).tap { |p| p.filter = period.filter }
172
+ } unless actual_period.nil?
173
+ end.compact
174
+ end
175
+
176
+ def covering_periods(time_start, time_end)
177
+ time_start -= total_period_length
178
+ time_end += total_period_length
179
+ @periods.map { |period| period.generate_in_range(time_start, time_end) }.flatten.sort
180
+ end
181
+
182
+ def period_matching_time(time)
183
+ @periods.detect { |p| p === time }.local_period(time)
184
+ end
185
+ end
186
+
187
+ end
188
+ end