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 +1 -0
- data/Manifest +30 -0
- data/README.rdoc +10 -0
- data/Rakefile +14 -0
- data/lib/computer_class_filter.rb +37 -0
- data/lib/deny_filter.rb +24 -0
- data/lib/filter.rb +43 -0
- data/lib/omega-tariffs-base.rb +13 -0
- data/lib/packet_filter.rb +111 -0
- data/lib/rate_filter.rb +31 -0
- data/lib/regioned_calculation.rb +61 -0
- data/lib/repeating_filter.rb +188 -0
- data/lib/settings.rb +42 -0
- data/lib/single_time_filter.rb +67 -0
- data/lib/tarif_builder.rb +16 -0
- data/lib/weekday_filter.rb +53 -0
- data/omega-tariffs-base.gemspec +36 -0
- data/spec/complex_tarif_spec.rb +54 -0
- data/spec/computer_class_filter_spec.rb +82 -0
- data/spec/deny_filter_spec.rb +23 -0
- data/spec/filter_spec.rb +36 -0
- data/spec/lib/active_record_classes_stub.rb +151 -0
- data/spec/lib/init.rb +5 -0
- data/spec/lib/spec_helper.rb +9 -0
- data/spec/packet_filter_spec.rb +287 -0
- data/spec/rate_filter_spec.rb +36 -0
- data/spec/repeating_filter_spec.rb +250 -0
- data/spec/settings_spec.rb +62 -0
- data/spec/single_time_filter_spec.rb +122 -0
- data/spec/tarif_builder_spec.rb +43 -0
- data/spec/weekday_filter_spec.rb +144 -0
- metadata +147 -0
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
|
data/lib/deny_filter.rb
ADDED
@@ -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
|
data/lib/rate_filter.rb
ADDED
@@ -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
|