1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
|
class Chouette::TimeTable < Chouette::TridentActiveRecord
include TimeTableRestrictions
# FIXME http://jira.codehaus.org/browse/JRUBY-6358
self.primary_key = "id"
acts_as_taggable
attr_accessor :monday,:tuesday,:wednesday,:thursday,:friday,:saturday,:sunday
attr_accessor :tag_search
def self.ransackable_attributes auth_object = nil
(column_names + ['tag_search']) + _ransackers.keys
end
has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney'
has_many :dates, -> {order(:date)}, inverse_of: :time_table, :validate => :true, :class_name => "Chouette::TimeTableDate", :dependent => :destroy
has_many :periods, -> {order(:period_start)}, inverse_of: :time_table, :validate => :true, :class_name => "Chouette::TimeTablePeriod", :dependent => :destroy
belongs_to :calendar
belongs_to :created_from, class_name: 'Chouette::TimeTable'
scope :overlapping, -> (date_start, date_end) do
joins(:periods).where('(period_start, period_end) OVERLAPS (?, ?)', date_start, date_end)
end
after_save :save_shortcuts
def self.object_id_key
"Timetable"
end
accepts_nested_attributes_for :dates, :allow_destroy => :true
accepts_nested_attributes_for :periods, :allow_destroy => :true
validates_presence_of :comment
validates_associated :dates
validates_associated :periods
def continuous_dates
chunk = {}
group = nil
self.dates.where(in_out: true).each_with_index do |date, index|
group ||= index
group = (date.date == dates[index - 1].date + 1.day) ? group : group + 1
chunk[group] ||= []
chunk[group] << date
end
chunk.values
end
def convert_continuous_dates_to_periods
chunks = self.continuous_dates
# Remove less than 3 continuous day chunk
chunks.delete_if {|chunk| chunk.count < 3}
transaction do
chunks.each do |chunk|
self.periods.create!(period_start: chunk.first.date, period_end: chunk.last.date)
chunk.map(&:destroy)
end
end
end
def state_update state
update_attributes(self.class.state_permited_attributes(state))
self.tag_list = state['tags'].collect{|t| t['name']}.join(', ')
days = state['day_types'].split(',')
Date::DAYNAMES.map(&:underscore).each do |name|
prefix = human_attribute_name(name).first(2)
send("#{name}=", days.include?(prefix))
end
saved_dates = Hash[self.dates.collect{ |d| [d.id, d.date]}]
cmonth = Date.parse(state['current_periode_range'])
state['current_month'].each do |d|
date = Date.parse(d['date'])
checked = d['include_date'] || d['excluded_date']
in_out = d['include_date'] ? true : false
date_id = saved_dates.key(date)
time_table_date = self.dates.find(date_id) if date_id
next if !checked && !time_table_date
# Destroy date if no longer checked
next if !checked && time_table_date.destroy
# Create new date
unless time_table_date
time_table_date = self.dates.create({in_out: in_out, date: date})
end
# Update in_out
if in_out != time_table_date.in_out
time_table_date.update_attributes({in_out: in_out})
end
end
self.state_update_periods state['time_table_periods']
self.save
end
def state_update_periods state_periods
state_periods.each do |item|
period = self.periods.find(item['id']) if item['id']
next if period && item['deleted'] && period.destroy
period ||= self.periods.build
period.period_start = Date.parse(item['period_start'])
period.period_end = Date.parse(item['period_end'])
if period.changed?
period.save
item['id'] = period.id
end
end
state_periods.delete_if {|item| item['deleted']}
end
def self.state_permited_attributes item
item.slice('comment', 'color').to_hash
end
def presenter
@presenter ||= ::TimeTablePresenter.new( self)
end
def self.start_validity_period
[Chouette::TimeTable.minimum(:start_date)].compact.min
end
def self.end_validity_period
[Chouette::TimeTable.maximum(:end_date)].compact.max
end
def month_inspect(date)
(date.beginning_of_month..date.end_of_month).map do |d|
{
day: I18n.l(d, format: '%A'),
date: d.to_s,
wday: d.wday,
wnumber: d.strftime("%W").to_s,
mday: d.mday,
include_date: include_in_dates?(d),
excluded_date: excluded_date?(d)
}
end
end
def save_shortcuts
shortcuts_update
self.update_column(:start_date, start_date)
self.update_column(:end_date, end_date)
end
def shortcuts_update(date=nil)
dates_array = bounding_dates
#if new_record?
if dates_array.empty?
self.start_date=nil
self.end_date=nil
else
self.start_date=dates_array.min
self.end_date=dates_array.max
end
#else
# if dates_array.empty?
# update_attributes :start_date => nil, :end_date => nil
# else
# update_attributes :start_date => dates_array.min, :end_date => dates_array.max
# end
#end
end
def validity_out_from_on?(expected_date)
return false unless self.end_date
self.end_date <= expected_date
end
def validity_out_between?(starting_date, ending_date)
return false unless self.start_date
starting_date < self.end_date &&
self.end_date <= ending_date
end
def self.validity_out_from_on?(expected_date,limit=0)
if limit==0
Chouette::TimeTable.where("end_date <= ?", expected_date)
else
Chouette::TimeTable.where("end_date <= ?", expected_date).limit( limit)
end
end
def self.validity_out_between?(start_date, end_date,limit=0)
if limit==0
Chouette::TimeTable.where( "? < end_date", start_date).where( "end_date <= ?", end_date)
else
Chouette::TimeTable.where( "? < end_date", start_date).where( "end_date <= ?", end_date).limit( limit)
end
end
# Return days which intersects with the time table dates and periods
def intersects(days)
[].tap do |intersect_days|
days.each do |day|
intersect_days << day if include_day?(day)
end
end
end
def include_day?(day)
include_in_dates?(day) || include_in_periods?(day)
end
def include_in_dates?(day)
self.dates.any?{ |d| d.date === day && d.in_out == true }
end
def excluded_date?(day)
self.dates.any?{ |d| d.date === day && d.in_out == false }
end
def include_in_periods?(day)
self.periods.any?{ |period| period.period_start <= day &&
day <= period.period_end &&
valid_days.include?(day.cwday) &&
! excluded_date?(day) }
end
def include_in_overlap_dates?(day)
return false if self.excluded_date?(day)
counter = self.dates.select{ |d| d.date === day}.size + self.periods.select{ |period| period.period_start <= day && day <= period.period_end && valid_days.include?(day.cwday) }.size
counter <= 1 ? false : true
end
def periods_max_date
return nil if self.periods.empty?
min_start = self.periods.map(&:period_start).compact.min
max_end = self.periods.map(&:period_end).compact.max
result = nil
if max_end && min_start
max_end.downto( min_start) do |date|
if self.valid_days.include?(date.cwday) && !self.excluded_date?(date)
result = date
break
end
end
end
result
end
def periods_min_date
return nil if self.periods.empty?
min_start = self.periods.map(&:period_start).compact.min
max_end = self.periods.map(&:period_end).compact.max
result = nil
if max_end && min_start
min_start.upto(max_end) do |date|
if self.valid_days.include?(date.cwday) && !self.excluded_date?(date)
result = date
break
end
end
end
result
end
def bounding_dates
bounding_min = self.dates.select{|d| d.in_out}.map(&:date).compact.min
bounding_max = self.dates.select{|d| d.in_out}.map(&:date).compact.max
unless self.periods.empty?
bounding_min = periods_min_date if periods_min_date &&
(bounding_min.nil? || (periods_min_date < bounding_min))
bounding_max = periods_max_date if periods_max_date &&
(bounding_max.nil? || (bounding_max < periods_max_date))
end
[bounding_min, bounding_max].compact
end
def day_by_mask(flag)
int_day_types & flag == flag
end
def self.day_by_mask(int_day_types,flag)
int_day_types & flag == flag
end
def valid_days
# Build an array with day of calendar week (1-7, Monday is 1).
[].tap do |valid_days|
valid_days << 1 if monday
valid_days << 2 if tuesday
valid_days << 3 if wednesday
valid_days << 4 if thursday
valid_days << 5 if friday
valid_days << 6 if saturday
valid_days << 7 if sunday
end
end
def self.valid_days(int_day_types)
# Build an array with day of calendar week (1-7, Monday is 1).
[].tap do |valid_days|
valid_days << 1 if day_by_mask(int_day_types,4)
valid_days << 2 if day_by_mask(int_day_types,8)
valid_days << 3 if day_by_mask(int_day_types,16)
valid_days << 4 if day_by_mask(int_day_types,32)
valid_days << 5 if day_by_mask(int_day_types,64)
valid_days << 6 if day_by_mask(int_day_types,128)
valid_days << 7 if day_by_mask(int_day_types,256)
end
end
def monday
day_by_mask(4)
end
def tuesday
day_by_mask(8)
end
def wednesday
day_by_mask(16)
end
def thursday
day_by_mask(32)
end
def friday
day_by_mask(64)
end
def saturday
day_by_mask(128)
end
def sunday
day_by_mask(256)
end
def set_day(day,flag)
if day == '1' || day == true
self.int_day_types |= flag
else
self.int_day_types &= ~flag
end
shortcuts_update
end
def monday=(day)
set_day(day,4)
end
def tuesday=(day)
set_day(day,8)
end
def wednesday=(day)
set_day(day,16)
end
def thursday=(day)
set_day(day,32)
end
def friday=(day)
set_day(day,64)
end
def saturday=(day)
set_day(day,128)
end
def sunday=(day)
set_day(day,256)
end
def effective_days_of_period(period,valid_days=self.valid_days)
days = []
period.period_start.upto(period.period_end) do |date|
if valid_days.include?(date.cwday) && !self.excluded_date?(date)
days << date
end
end
days
end
def effective_days(valid_days=self.valid_days)
days=self.effective_days_of_periods(valid_days)
self.dates.each do |d|
days |= [d.date] if d.in_out
end
days.sort
end
def effective_days_of_periods(valid_days=self.valid_days)
days = []
self.periods.each { |p| days |= self.effective_days_of_period(p,valid_days)}
days.sort
end
def clone_periods
periods = []
self.periods.each { |p| periods << p.copy}
periods
end
def included_days
days = []
self.dates.each do |d|
days |= [d.date] if d.in_out
end
days.sort
end
def excluded_days
days = []
self.dates.each do |d|
days |= [d.date] unless d.in_out
end
days.sort
end
# produce a copy of periods without anyone overlapping or including another
def optimize_periods
periods = self.clone_periods
optimized = []
i=0
while i < periods.length
p1 = periods[i]
optimized << p1
j= i+1
while j < periods.length
p2 = periods[j]
if p1.contains? p2
periods.delete p2
elsif p1.overlap? p2
p1.period_start = [p1.period_start,p2.period_start].min
p1.period_end = [p1.period_end,p2.period_end].max
periods.delete p2
else
j += 1
end
end
i+= 1
end
optimized.sort { |a,b| a.period_start <=> b.period_start}
end
# add a peculiar day or switch it from excluded to included
def add_included_day(d)
if self.excluded_date?(d)
self.dates.each do |date|
if date.date === d
date.in_out = true
end
end
elsif !self.include_in_dates?(d)
self.dates << Chouette::TimeTableDate.new(:date => d, :in_out => true)
end
end
# merge effective days from another timetable
def merge!(another_tt)
transaction do
# if one tt has no period, just merge lists
if self.periods.empty? || another_tt.periods.empty?
if !another_tt.periods.empty?
# copy periods
self.periods = another_tt.clone_periods
# set valid_days
self.int_day_types = another_tt.int_day_types
end
# merge dates
self.dates ||= []
another_tt.included_days.each do |d|
add_included_day d
end
else
# check if periods can be kept
common_day_types = self.int_day_types & another_tt.int_day_types & 508
# if common day types : merge periods
if common_day_types != 0
periods = self.optimize_periods
another_periods = another_tt.optimize_periods
# add not common days of both periods as peculiar days
self.effective_days_of_periods(self.class.valid_days(self.int_day_types ^ common_day_types)).each do |d|
self.dates |= [Chouette::TimeTableDate.new(:date => d, :in_out => true)]
end
another_tt.effective_days_of_periods(self.class.valid_days(another_tt.int_day_types ^ common_day_types)).each do |d|
add_included_day d
end
# merge periods
self.periods = periods | another_periods
self.int_day_types = common_day_types
self.periods = self.optimize_periods
else
# convert all period in days
self.effective_days_of_periods.each do |d|
self.dates << Chouette::TimeTableDate.new(:date => d, :in_out => true) unless self.include_in_dates?(d)
end
another_tt.effective_days_of_periods.each do |d|
add_included_day d
end
end
end
# if remained excluded dates are valid in other tt , remove it from result
self.dates.each do |date|
date.in_out = true if date.in_out == false && another_tt.include_day?(date.date)
end
# if peculiar dates are valid in new periods, remove them
if !self.periods.empty?
days_in_period = self.effective_days_of_periods
dates = []
self.dates.each do |date|
dates << date unless date.in_out && days_in_period.include?(date.date)
end
self.dates = dates
end
self.dates.to_a.sort! { |a,b| a.date <=> b.date}
self.save!
end
self.convert_continuous_dates_to_periods
end
# remove dates form tt which aren't in another_tt
def intersect!(another_tt)
transaction do
# transform tt as effective dates and get common ones
days = another_tt.intersects(self.effective_days) & self.intersects(another_tt.effective_days)
self.dates.clear
days.each {|d| self.dates << Chouette::TimeTableDate.new( :date =>d, :in_out => true)}
self.periods.clear
self.int_day_types = 0
self.dates.to_a.sort! { |a,b| a.date <=> b.date}
self.save!
end
self.convert_continuous_dates_to_periods
end
def disjoin!(another_tt)
transaction do
# remove days from another calendar
days_to_exclude = self.intersects(another_tt.effective_days)
days = self.effective_days - days_to_exclude
self.dates.clear
self.periods.clear
self.int_day_types = 0
days.each {|d| self.dates << Chouette::TimeTableDate.new( :date =>d, :in_out => true)}
self.dates.to_a.sort! { |a,b| a.date <=> b.date}
self.periods.to_a.sort! { |a,b| a.period_start <=> b.period_start}
self.save!
end
self.convert_continuous_dates_to_periods
end
def duplicate
tt = self.deep_clone :include => [:periods, :dates], :except => :object_version
tt.uniq_objectid
tt.tag_list.add(*self.tag_list) unless self.tag_list.empty?
tt.created_from = self
tt.comment = I18n.t("activerecord.copy", :name => self.comment)
tt
end
end
|