rail_feeds 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +31 -0
  5. data/.travis.yml +26 -0
  6. data/CHANGELOG.md +3 -0
  7. data/Gemfile +6 -0
  8. data/Guardfile +25 -0
  9. data/LICENSE.md +32 -0
  10. data/README.md +77 -0
  11. data/Rakefile +3 -0
  12. data/doc/guides/Logging.md +13 -0
  13. data/doc/guides/Network Rail/CORPUS.md +34 -0
  14. data/doc/guides/Network Rail/SMART.md +39 -0
  15. data/doc/guides/Network Rail/Schedule.md +138 -0
  16. data/file +0 -0
  17. data/lib/rail_feeds/credentials.rb +45 -0
  18. data/lib/rail_feeds/logging.rb +51 -0
  19. data/lib/rail_feeds/network_rail/corpus.rb +77 -0
  20. data/lib/rail_feeds/network_rail/credentials.rb +22 -0
  21. data/lib/rail_feeds/network_rail/http_client.rb +57 -0
  22. data/lib/rail_feeds/network_rail/schedule/association.rb +208 -0
  23. data/lib/rail_feeds/network_rail/schedule/data.rb +215 -0
  24. data/lib/rail_feeds/network_rail/schedule/days.rb +95 -0
  25. data/lib/rail_feeds/network_rail/schedule/fetcher.rb +193 -0
  26. data/lib/rail_feeds/network_rail/schedule/header/cif.rb +102 -0
  27. data/lib/rail_feeds/network_rail/schedule/header/json.rb +79 -0
  28. data/lib/rail_feeds/network_rail/schedule/header.rb +22 -0
  29. data/lib/rail_feeds/network_rail/schedule/parser/cif.rb +141 -0
  30. data/lib/rail_feeds/network_rail/schedule/parser/json.rb +87 -0
  31. data/lib/rail_feeds/network_rail/schedule/parser.rb +108 -0
  32. data/lib/rail_feeds/network_rail/schedule/stp_indicator.rb +72 -0
  33. data/lib/rail_feeds/network_rail/schedule/tiploc.rb +100 -0
  34. data/lib/rail_feeds/network_rail/schedule/train_schedule/change_en_route.rb +158 -0
  35. data/lib/rail_feeds/network_rail/schedule/train_schedule/location/intermediate.rb +119 -0
  36. data/lib/rail_feeds/network_rail/schedule/train_schedule/location/origin.rb +91 -0
  37. data/lib/rail_feeds/network_rail/schedule/train_schedule/location/terminating.rb +72 -0
  38. data/lib/rail_feeds/network_rail/schedule/train_schedule/location.rb +76 -0
  39. data/lib/rail_feeds/network_rail/schedule/train_schedule.rb +392 -0
  40. data/lib/rail_feeds/network_rail/schedule.rb +33 -0
  41. data/lib/rail_feeds/network_rail/smart.rb +186 -0
  42. data/lib/rail_feeds/network_rail/stomp_client.rb +77 -0
  43. data/lib/rail_feeds/network_rail.rb +16 -0
  44. data/lib/rail_feeds/version.rb +14 -0
  45. data/lib/rail_feeds.rb +10 -0
  46. data/rail_feeds.gemspec +32 -0
  47. data/spec/fixtures/network_rail/schedule/data/full.yaml +60 -0
  48. data/spec/fixtures/network_rail/schedule/data/starting.yaml +131 -0
  49. data/spec/fixtures/network_rail/schedule/data/update-gap.yaml +10 -0
  50. data/spec/fixtures/network_rail/schedule/data/update-next.yaml +13 -0
  51. data/spec/fixtures/network_rail/schedule/data/update-old.yaml +10 -0
  52. data/spec/fixtures/network_rail/schedule/data/update.yaml +112 -0
  53. data/spec/fixtures/network_rail/schedule/parser/train_create.json +1 -0
  54. data/spec/fixtures/network_rail/schedule/parser/train_delete.json +1 -0
  55. data/spec/fixtures/network_rail/schedule/train_schedule/json-data.yaml +67 -0
  56. data/spec/rail_feeds/credentials_spec.rb +46 -0
  57. data/spec/rail_feeds/logging_spec.rb +81 -0
  58. data/spec/rail_feeds/network_rail/corpus_spec.rb +92 -0
  59. data/spec/rail_feeds/network_rail/credentials_spec.rb +22 -0
  60. data/spec/rail_feeds/network_rail/http_client_spec.rb +88 -0
  61. data/spec/rail_feeds/network_rail/schedule/association_spec.rb +205 -0
  62. data/spec/rail_feeds/network_rail/schedule/data_spec.rb +219 -0
  63. data/spec/rail_feeds/network_rail/schedule/days_shared.rb +99 -0
  64. data/spec/rail_feeds/network_rail/schedule/days_spec.rb +4 -0
  65. data/spec/rail_feeds/network_rail/schedule/fetcher_spec.rb +228 -0
  66. data/spec/rail_feeds/network_rail/schedule/header/cif_spec.rb +72 -0
  67. data/spec/rail_feeds/network_rail/schedule/header/json_spec.rb +51 -0
  68. data/spec/rail_feeds/network_rail/schedule/header_spec.rb +19 -0
  69. data/spec/rail_feeds/network_rail/schedule/parser/cif_spec.rb +197 -0
  70. data/spec/rail_feeds/network_rail/schedule/parser/json_spec.rb +172 -0
  71. data/spec/rail_feeds/network_rail/schedule/parser_spec.rb +34 -0
  72. data/spec/rail_feeds/network_rail/schedule/stp_indicator_shared.rb +49 -0
  73. data/spec/rail_feeds/network_rail/schedule/stp_indicator_spec.rb +4 -0
  74. data/spec/rail_feeds/network_rail/schedule/tiploc_spec.rb +77 -0
  75. data/spec/rail_feeds/network_rail/schedule/train_schedule/change_en_route_spec.rb +121 -0
  76. data/spec/rail_feeds/network_rail/schedule/train_schedule/location/intermediate_spec.rb +95 -0
  77. data/spec/rail_feeds/network_rail/schedule/train_schedule/location/origin_spec.rb +87 -0
  78. data/spec/rail_feeds/network_rail/schedule/train_schedule/location/terminating_spec.rb +81 -0
  79. data/spec/rail_feeds/network_rail/schedule/train_schedule/location_spec.rb +35 -0
  80. data/spec/rail_feeds/network_rail/schedule/train_schedule_spec.rb +284 -0
  81. data/spec/rail_feeds/network_rail/schedule_spec.rb +41 -0
  82. data/spec/rail_feeds/network_rail/smart_spec.rb +194 -0
  83. data/spec/rail_feeds/network_rail/stomp_client_spec.rb +151 -0
  84. data/spec/rail_feeds/network_rail_spec.rb +7 -0
  85. data/spec/rail_feeds_spec.rb +11 -0
  86. data/spec/spec_helper.rb +47 -0
  87. metadata +282 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+
5
+ module RailFeeds
6
+ module NetworkRail
7
+ # A wrapper class for ::Net::HTTP
8
+ class HTTPClient
9
+ include Logging
10
+
11
+ HOST = 'datafeeds.networkrail.co.uk'
12
+
13
+ # Initialize a new http client.
14
+ # @param [RailFeeds::NetworkRail::Credentials] credentials
15
+ # The credentials for connecting to the feed.
16
+ # @param [Logger] logger
17
+ # The logger for outputting evetns, if nil the global logger will be used.
18
+ def initialize(credentials: Credentials, logger: nil)
19
+ @credentials = credentials
20
+ self.logger = logger unless logger.nil?
21
+ end
22
+
23
+ # Fetch path from network rail server.
24
+ # @param [String] path The path to fetch.
25
+ # @yield [file] Once the block has run the temp file will be deleted.
26
+ # @yieldparam [Tempfile] file The content of the file.
27
+ def fetch(path)
28
+ logger.debug "fetch(#{path.inspect})"
29
+ uri = URI("https://#{HOST}/#{path}")
30
+ yield uri.open(http_basic_authentication: @credentials.to_a)
31
+ end
32
+
33
+ # Fetch path from network rail server and unzip it.
34
+ # @param [String] path The path to fetch.
35
+ # @yield [reader] Once the block has run the temp file will be deleted.
36
+ # @yieldparam [Zlib::GzipReader] reader The unzippable content of the file.
37
+ def fetch_unzipped(path)
38
+ logger.debug "get_unzipped(#{path.inspect})"
39
+ fetch(path) do |gz_file|
40
+ yield Zlib::GzipReader.open(gz_file.path)
41
+ end
42
+ end
43
+
44
+ # Download path from netwrok rail server.
45
+ # @param [String] path The path to download.
46
+ # @param [String] file The path to the file to save the contents in.
47
+ def download(path, file)
48
+ logger.debug "download(#{path.inspect}, #{file.inspect})"
49
+ fetch(path) do |src|
50
+ File.open(file, 'w') do |dst|
51
+ IO.copy_stream src, dst
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailFeeds
4
+ module NetworkRail
5
+ module Schedule
6
+ # rubocop:disable Metrics/ClassLength
7
+ # A class for holding information about an association between many trains.
8
+ class Association
9
+ include Comparable
10
+ include Schedule::Days
11
+ include Schedule::STPIndicator
12
+
13
+ # @!attribute [rw] main_train_uid
14
+ # @return [String] The UID of the main train in the association.
15
+ # @!attribute [rw] associated_train_uid
16
+ # @return [String] The UID of the associated train in the association.
17
+ # @!attribute [rw] category
18
+ # @return [String] The category of the association:
19
+ # * JJ - join
20
+ # * VV - divide
21
+ # * NP - next
22
+ # @!attribute [rw] start_date
23
+ # @return [Date] When the schedule starts.
24
+ # @!attribute [rw] end_date
25
+ # @return [Date] When the schedule ends.
26
+ # @!attribute [rw] date_indicator
27
+ # @return [String] When the assocation happens:
28
+ # * S - same day
29
+ # * N - over next midnight
30
+ # * P - over previous midnight
31
+ # @!attribute [rw] days
32
+ # @return [Array<Boolean>] The days on which the service runs.
33
+ # [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday]
34
+ # @!attribute [rw] tiploc
35
+ # @return [String] The TIPLOC of the location the association occurs.
36
+ # @!attribute [rw] base_location_suffix
37
+ # @return [String, nil]
38
+ # Together with the tiploc uniquely identifies the association
39
+ # on the base_uid.
40
+ # @!attribute [rw] associated_location_suffix
41
+ # @return [String, nil]
42
+ # Together with the tiploc uniquely identifies the association
43
+ # on the associated_uid.
44
+ # @!attribute [rw] stp_indicator
45
+ # @return [String]
46
+ # * C - cancellation of permanent schedule
47
+ # * N - new STP schedule
48
+ # * O - STP overlay of permanent schedule
49
+ # * P - permanent
50
+
51
+ attr_accessor :main_train_uid, :associated_train_uid, :category,
52
+ :start_date, :end_date, :date_indicator,
53
+ :tiploc, :main_location_suffix, :associated_location_suffix
54
+ # Attributes from modules :days, :stp_indicator
55
+
56
+ def initialize(**attributes)
57
+ attributes.each do |attribute, value|
58
+ send "#{attribute}=", value
59
+ end
60
+ end
61
+
62
+ # rubocop:disable Metrics/AbcSize
63
+ # rubocop:disable Metrics/MethodLength
64
+ # Initialize a new association from a CIF file line
65
+ def self.from_cif(line)
66
+ unless %w[AAN AAR AAD].include?(line[0..2])
67
+ fail ArgumentError, "Invalid line:\n#{line}"
68
+ end
69
+
70
+ new(
71
+ main_train_uid: line[3..8].strip,
72
+ associated_train_uid: line[9..14].strip,
73
+ start_date: Schedule.make_date(line[15..20]),
74
+ end_date: Schedule.make_date(line[21..26], allow_nil: line[2].eql?('D')),
75
+ days: days_from_cif(line[27..33]),
76
+ category: Schedule.nil_or_strip(line[34..35]),
77
+ date_indicator: Schedule.nil_or_strip(line[36]),
78
+ tiploc: line[37..43].strip,
79
+ main_location_suffix: Schedule.nil_or_i(line[44]),
80
+ associated_location_suffix: Schedule.nil_or_i(line[45]),
81
+ stp_indicator: stp_indicator_from_cif(line[79])
82
+ )
83
+ end
84
+ # rubocop:enable Metrics/AbcSize
85
+ # rubocop:enable Metrics/MethodLength
86
+
87
+ # rubocop:disable Metrics/AbcSize
88
+ # Initialize a new association from a JSON file line
89
+ def self.from_json(line)
90
+ data = ::JSON.parse(line)['JsonAssociationV1']
91
+
92
+ new(
93
+ main_train_uid: data['main_train_uid'],
94
+ associated_train_uid: data['assoc_train_uid'],
95
+ start_date: Date.parse(data['assoc_start_date']),
96
+ end_date: data['assoc_end_date'] ? Date.parse(data['assoc_end_date']) : nil,
97
+ days: days_from_cif(data['assoc_days']),
98
+ category: Schedule.nil_or_strip(data['category']),
99
+ date_indicator: Schedule.nil_or_strip(data['date_indicator']),
100
+ tiploc: data['location'],
101
+ main_location_suffix: Schedule.nil_or_i(data['base_location_suffix']),
102
+ associated_location_suffix: Schedule.nil_or_i(data['assoc_location_suffix']),
103
+ stp_indicator: stp_indicator_from_cif(data['CIF_stp_indicator'])
104
+ )
105
+ end
106
+ # rubocop:enable Metrics/AbcSize
107
+
108
+ # Test if this is a join association.
109
+ def join?
110
+ category.eql?('JJ')
111
+ end
112
+
113
+ # Test if this is a divide association.
114
+ def divide?
115
+ category.eql?('VV')
116
+ end
117
+
118
+ # Test if this is a next association.
119
+ def next?
120
+ category.eql?('NP')
121
+ end
122
+
123
+ # Test if the association happens on the same day.
124
+ def same_day?
125
+ date_indicator.eql?('S')
126
+ end
127
+
128
+ # Test if the association happens over the next midnight.
129
+ def over_next_midnight?
130
+ date_indicator.eql?('N')
131
+ end
132
+
133
+ # Test if the association happens over the previous midnight.
134
+ def over_previous_midnight?
135
+ date_indicator.eql?('P')
136
+ end
137
+
138
+ # Uniquely identifies the event on the main_train_uid
139
+ def main_train_event_id
140
+ "#{tiploc}-#{main_location_suffix}"
141
+ end
142
+
143
+ # Uniquely identifies the event on the associated_train_uid
144
+ def associated_train_event_id
145
+ "#{tiploc}-#{associated_location_suffix}"
146
+ end
147
+
148
+ def ==(other)
149
+ main_train_event_id == other&.main_train_event_id &&
150
+ associated_train_event_id == other&.associated_train_event_id
151
+ end
152
+
153
+ def <=>(other)
154
+ start_date <=> other&.start_date
155
+ end
156
+
157
+ def hash
158
+ "#{tiploc}-#{main_location_suffix}-#{associated_location_suffix}"
159
+ end
160
+
161
+ # rubocop:disable Metrics/AbcSize
162
+ def to_cif
163
+ format('%-80.80s', [
164
+ 'AAN',
165
+ format('%-6.6s', main_train_uid),
166
+ format('%-6.6s', associated_train_uid),
167
+ # rubocop:disable Style/FormatStringToken
168
+ format('%-6.6s', start_date&.strftime('%y%m%d')),
169
+ format('%-6.6s', end_date&.strftime('%y%m%d')),
170
+ # rubocop:enable Style/FormatStringToken
171
+ days_to_cif,
172
+ format('%-2.2s', category),
173
+ format('%-1.1s', date_indicator),
174
+ format('%-7.7s', tiploc),
175
+ format('%-1.1s', main_location_suffix),
176
+ format('%-1.1s', associated_location_suffix),
177
+ 'T ',
178
+ stp_indicator_to_cif
179
+ ].join) + "\n"
180
+ end
181
+ # rubocop:enable Metrics/AbcSize
182
+
183
+ # rubocop:disable Metrics/MethodLength
184
+ def to_json
185
+ {
186
+ 'JsonAssociationV1' => {
187
+ 'transaction_type' => 'Create',
188
+ 'main_train_uid' => main_train_uid,
189
+ 'assoc_train_uid' => associated_train_uid,
190
+ 'assoc_start_date' => start_date.strftime('%Y-%m-%dT00:00:00Z'),
191
+ 'assoc_end_date' => end_date.strftime('%Y-%m-%dT00:00:00Z'),
192
+ 'assoc_days' => days_to_cif,
193
+ 'category' => category,
194
+ 'date_indicator' => date_indicator,
195
+ 'location' => tiploc,
196
+ 'base_location_suffix' => main_location_suffix,
197
+ 'assoc_location_suffix' => associated_location_suffix,
198
+ 'diagram_type' => 'T',
199
+ 'CIF_stp_indicator' => stp_indicator_to_cif
200
+ }
201
+ }.to_json
202
+ end
203
+ # rubocop:enable Metrics/MethodLength
204
+ end
205
+ # rubocop:enable Metrics/ClassLength
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailFeeds
4
+ module NetworkRail
5
+ module Schedule
6
+ # rubocop:disable Metrics/ClassLength
7
+ # A class for holding schedule data read from schedule file(s).
8
+ class Data
9
+ include Logging
10
+
11
+ # @!attribute [r] last_header The last header added.
12
+ # @return [RailFeeds::NetworkRail::Schedule::Header::CIF]
13
+ # @!attribute [r] associations
14
+ # @return [Hash<RailFeeds::NetworkRail::Schedule::Association>]
15
+ # @!attribute [r] tiplocs
16
+ # @return [Hash<RailFeeds::NetworkRail::Schedule::Tiploc>]
17
+ # @!attribute [r] trains
18
+ # @return [Hash{String=>RailFeeds::NetworkRail::Schedule::TrainSchedule}]
19
+ # Schedules grouped by the train's UID
20
+
21
+ attr_accessor :last_header, :associations, :tiplocs, :trains
22
+
23
+ # rubocop:disable Metrics/AbcSize
24
+ # rubocop:disable Metrics/MethodLength
25
+ # Initialize a new data.
26
+ # @param [Logger, nil] logger
27
+ # The logger for outputting events, if nil the global logger is used.
28
+ def initialize(logger: nil)
29
+ self.logger = logger unless logger.nil?
30
+ @parser = Parser::CIF.new(
31
+ logger: logger,
32
+ on_header: proc { |*args| do_header(*args) },
33
+ on_trailer: proc { |*args| do_trailer(*args) },
34
+ on_tiploc_create: proc { |*args| do_tiploc_create(*args) },
35
+ on_tiploc_update: proc { |*args| do_tiploc_update(*args) },
36
+ on_tiploc_delete: proc { |*args| do_tiploc_delete(*args) },
37
+ on_association_create: proc { |*args| do_association_create(*args) },
38
+ on_association_update: proc { |*args| do_association_update(*args) },
39
+ on_association_delete: proc { |*args| do_association_delete(*args) },
40
+ on_train_schedule_create: proc { |*args| do_train_schedule_create(*args) },
41
+ on_train_schedule_update: proc { |*args| do_train_schedule_update(*args) },
42
+ on_train_schedule_delete: proc { |*args| do_train_schedule_delete(*args) }
43
+ )
44
+ reset_data
45
+ end
46
+ # rubocop:enable Metrics/AbcSize
47
+ # rubocop:enable Metrics/MethodLength
48
+
49
+ # Load data files into the parser, of types:
50
+ # * Full CIF file - the data will be replaced
51
+ # * Update CIF file - the data will be changed
52
+ # @param [IO] file
53
+ # The file to load data from.
54
+ def load_cif_file(file)
55
+ @parser.parse_cif_file file
56
+
57
+ logger.info "Currently have #{associations.count} associations, " \
58
+ "#{tiplocs.count} tiplocs, #{trains.count} trains."
59
+ end
60
+
61
+ # rubocop:disable Metrics/AbcSize
62
+ # rubocop:disable Metrics/MethodLength
63
+ # Get the contained data in CIF format
64
+ # Expects a block to receive each line
65
+ def generate_cif
66
+ fail 'No loaded data' if last_header.nil?
67
+
68
+ header = Header::CIF.new(
69
+ extracted_at: last_header.extracted_at,
70
+ update_indicator: 'F',
71
+ start_date: last_header.start_date,
72
+ end_date: last_header.end_date
73
+ )
74
+
75
+ yield "/!! Start of file\n"
76
+ yield "/!! Generated: #{header.extracted_at.utc&.strftime('%d/%m/%Y %H:%M')}\n"
77
+ yield header.to_cif
78
+ tiplocs.values.sort.each { |tiploc| yield tiploc.to_cif }
79
+ associations.values.sort.each { |association| yield association.to_cif }
80
+ trains.values.flatten.sort.each do |train_schedule|
81
+ train_schedule.to_cif.each_line { |line| yield line }
82
+ end
83
+ yield "ZZ#{' ' * 78}\n"
84
+ yield "/!! End of file\n"
85
+ end
86
+ # rubocop:enable Metrics/AbcSize
87
+ # rubocop:enable Metrics/MethodLength
88
+
89
+ # Fetch data over the web.
90
+ # Gets the feed of all trains.
91
+ # @param [RailFeeds::NetworkRail::Credentials] credentials
92
+ # The credentials for connecting to the feed.
93
+ # @return [RailFeeds::NetworkRail::Schedule::Header::CIF]
94
+ # The header of the last file added.
95
+ def fetch_data(credentials: Credentials)
96
+ fetcher = Fetcher.new credentials: credentials
97
+
98
+ method = if last_header.nil? ||
99
+ last_header.extracted_at.to_date < Date.today - 6
100
+ # Need to get a full andthen updates
101
+ :fetch_all
102
+ else
103
+ # Can only get updates
104
+ :fetch_all_updates
105
+ end
106
+
107
+ fetcher.send(method, :cif) do |file|
108
+ load_cif_file file
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def reset_data
115
+ @last_header = nil
116
+ @associations = {}
117
+ @tiplocs = {}
118
+ @trains = {}
119
+ end
120
+
121
+ # rubocop:disable Metrics/AbcSize
122
+ # rubocop:disable Metrics/CyclomaticComplexity
123
+ # rubocop:disable Metrics/MethodLength
124
+ # rubocop:disable Metrics/PerceivedComplexity
125
+ def ensure_correct_update_order(header)
126
+ if last_header.nil?
127
+ # No data whatsoever - this must be a full extract
128
+ unless header.full?
129
+ fail ArgumentError,
130
+ 'Update can\'t be loaded before loading a full extract.'
131
+ end
132
+
133
+ elsif last_header.update? && header.update?
134
+ # Check against last update
135
+ if header.extracted_at < last_header.extracted_at
136
+ fail ArgumentError,
137
+ 'Update is too old, it is before the last applied update.'
138
+ end
139
+ if header.previous_file_reference != last_header.current_file_reference
140
+ fail ArgumentError,
141
+ 'Missing update(s). Last applied update is ' \
142
+ "#{last_header.current_file_reference.inspect}, " \
143
+ "this update requires #{header.previous_file_reference.inspect} " \
144
+ 'to be the previous applied update.'
145
+ end
146
+ end
147
+ end
148
+ # rubocop:enable Metrics/AbcSize
149
+ # rubocop:enable Metrics/CyclomaticComplexity
150
+ # rubocop:enable Metrics/MethodLength
151
+ # rubocop:enable Metrics/PerceivedComplexity
152
+
153
+ # Header record
154
+ def do_header(_parser, header)
155
+ ensure_correct_update_order header
156
+ reset_data if header.full?
157
+ @last_header = header
158
+ end
159
+
160
+ # TIPLOC Insert record
161
+ def do_tiploc_create(_parser, tiploc)
162
+ tiplocs[tiploc.hash] = tiploc
163
+ end
164
+
165
+ # TIPLOC Amend record
166
+ def do_tiploc_update(_parser, tiploc_id, tiploc)
167
+ tiplocs[tiploc_id] = tiploc
168
+ end
169
+
170
+ # TIPLOC Delete record
171
+ def do_tiploc_delete(_parser, tiploc)
172
+ tiplocs.delete tiploc.hash
173
+ end
174
+
175
+ # Association New record
176
+ def do_association_create(_parser, association)
177
+ associations[association.hash] = association
178
+ end
179
+
180
+ # Association Revise record
181
+ def do_association_update(_parser, association)
182
+ associations[association.hash] = association
183
+ end
184
+
185
+ # Association Delete record
186
+ def do_association_delete(_parser, association)
187
+ associations.delete association.hash
188
+ end
189
+
190
+ # New Train received
191
+ def do_train_schedule_create(_parser, train_schedule)
192
+ trains[train_schedule.uid] ||= []
193
+ trains[train_schedule.uid].push train_schedule
194
+ end
195
+
196
+ # Revise Train received
197
+ def do_train_schedule_update(parser, train_schedule)
198
+ trains[train_schedule.uid] ||= []
199
+ index = trains[train_schedule.uid].index train_schedule
200
+ return do_train_schedule_create(parser, train_schedule) if index.nil?
201
+ trains[train_schedule.uid][index] = train_schedule
202
+ end
203
+
204
+ # Delete Train record
205
+ def do_train_schedule_delete(_parser, train_schedule)
206
+ trains[train_schedule.uid]&.delete train_schedule
207
+ end
208
+
209
+ # Trailer record
210
+ def do_trailer(_parser); end
211
+ end
212
+ # rubocop:enable Metrics/ClassLength
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailFeeds
4
+ module NetworkRail
5
+ module Schedule
6
+ # A collection of methods for working with a days array.
7
+ # Provides a days attribute to the class.
8
+ module Days
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ # @return [Array<Boolean, nil>] What days the record applies to
14
+ # (Monday -> Sunday).
15
+ def days
16
+ @days ||= Array.new(7, nil)
17
+ end
18
+
19
+ # @param [Array<Boolean, nil>, #to_s] value What days the record applies to.
20
+ # (Monday -> Sunday).
21
+ def days=(value)
22
+ value = days_from_cif(value) unless value.is_a?(Array)
23
+ (0..6).each do |i|
24
+ days[i] = value[i]&.&(true)
25
+ end
26
+ days
27
+ end
28
+
29
+ # Query if the record applies on Mondays
30
+ # @return [Boolean, nil]
31
+ def mondays?
32
+ days[0]
33
+ end
34
+
35
+ # Query if the record applies on Tuesdays
36
+ # @return [Boolean, nil]
37
+ def tuesdays?
38
+ days[1]
39
+ end
40
+
41
+ # Query if the record applies on Wednesdays
42
+ # @return [Boolean, nil]
43
+ def wednesdays?
44
+ days[2]
45
+ end
46
+
47
+ # Query if the record applies on Thursdays
48
+ # @return [Boolean, nil]
49
+ def thursdays?
50
+ days[3]
51
+ end
52
+
53
+ # Query if the record applies on Fridays
54
+ # @return [Boolean, nil]
55
+ def fridays?
56
+ days[4]
57
+ end
58
+
59
+ # Query if the record applies on Saturdays
60
+ # @return [Boolean, nil]
61
+ def saturdays?
62
+ days[5]
63
+ end
64
+
65
+ # Query if the record applies on Sundays
66
+ # @return [Boolean, nil]
67
+ def sundays?
68
+ days[6]
69
+ end
70
+
71
+ protected
72
+
73
+ def days_to_cif
74
+ self.class.days_to_cif days
75
+ end
76
+
77
+ def days_from_cif(value)
78
+ self.days = self.class.days_from_cif value
79
+ end
80
+
81
+ module ClassMethods # :nodoc:
82
+ def days_to_cif(value)
83
+ value.map { |d| d ? '1' : '0' }.join
84
+ end
85
+
86
+ def days_from_cif(value)
87
+ return [nil, nil, nil, nil, nil, nil, nil] if value.nil?
88
+ Array.new(7) { |i| value[i]&.eql?('1') }
89
+ end
90
+ end
91
+ private_constant :ClassMethods
92
+ end
93
+ end
94
+ end
95
+ end