omega-tariffs-base 0.1.5

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.
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