rail_feeds 0.0.1

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