gtfs-reader 0.2.2

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.
@@ -0,0 +1,72 @@
1
+ module GtfsReader
2
+ module Config
3
+ # Defines a single column in a {FileDefinition file}.
4
+ class Column
5
+ # A "parser" which simply returns its input. Used by default
6
+ IDENTITY_PARSER = ->(arg) { arg }
7
+
8
+ attr_reader :name
9
+
10
+ #@param name [String] the name of the column
11
+ #@option opts [Boolean] :required (false) If this column is required to
12
+ # appear in the given file
13
+ #@option opts [String] :alias an alternative name for this column. Many
14
+ # column names are needlessly prefixed with their filename:
15
+ # +Stop.stop_name+ could be aliased to +Stop.name+ for example.
16
+ #@option opts [Boolean] :unique (false) if values in this column need to be
17
+ # unique among all rows in the file.
18
+ def initialize(name, opts={}, &parser)
19
+ @name = name
20
+ @parser = block_given? ? parser : IDENTITY_PARSER
21
+
22
+ @opts = {
23
+ required: false,
24
+ unique: false,
25
+ alias: nil
26
+ }.merge (opts || {})
27
+ end
28
+
29
+ def parser(&block)
30
+ @parser = block if block_given?
31
+ @parser
32
+ end
33
+
34
+ #@return [Boolean] if this column is required to appear in the file
35
+ def required?
36
+ @opts[:required]
37
+ end
38
+
39
+ #@return [Boolean] if values in this column need to be unique among all rows
40
+ # in the file.
41
+ def unique?
42
+ @opts[:unique]
43
+ end
44
+
45
+ #@return [String,nil] this column's name's alias, if there is one
46
+ def alias
47
+ @opts[:alias]
48
+ end
49
+
50
+ #@return [Boolean]
51
+ def parser?
52
+ parser != IDENTITY_PARSER
53
+ end
54
+
55
+ def to_s
56
+ opts = @opts.collect do |key,value|
57
+ case value
58
+ when true then key
59
+ when false,nil then nil
60
+ else "#{key}=#{value}"
61
+ end
62
+ end.reject &:nil?
63
+
64
+ opts << 'has_parser' if parser?
65
+
66
+ str = name.to_s
67
+ str << ": #{opts.join ', '}" unless opts.empty?
68
+ "[#{str}]"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,208 @@
1
+ require_relative '../feed_definition'
2
+
3
+ module GtfsReader
4
+ module Config
5
+ module Defaults
6
+ FEED_DEFINITION = FeedDefinition.new.tap do |feed|
7
+ feed.instance_exec do
8
+ file :agency, required: true do
9
+ prefix :agency do
10
+ col :name, required: true
11
+ col :url, required: true
12
+ col :timezone, required: true
13
+ col :id, unique: true
14
+ col :lang
15
+ col :phone
16
+ col :fare_url
17
+ end
18
+ end
19
+
20
+ file :stops, required: true do
21
+ prefix :stop do
22
+ col :id, required: true, unique: true
23
+ col :code
24
+ col :name, required: true
25
+ col :desc
26
+ col :lat, required: true
27
+ col :lon, required: true
28
+ col :url
29
+ col :timezone
30
+ end
31
+
32
+ col :zone_id
33
+ col :location_type, &output_map( :stop, station: ?1 )
34
+ col :parent_station
35
+
36
+ col :wheelchair_boarding do |val|
37
+ if parent_station
38
+ case val
39
+ when ?2 then :no
40
+ when ?1 then :yes
41
+ else :inherit
42
+ end
43
+ else
44
+ case val
45
+ when ?2 then :no_vehicles
46
+ when ?1 then :some_vehicles
47
+ else :unknown
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ file :routes, required: true do
54
+ prefix :route do
55
+ col :id, required: true, unique: true
56
+ col :short_name, required: true
57
+ col :long_name, required: true
58
+ col :desc
59
+ col :type, required: true,
60
+ &output_map( :unknown,
61
+ tram: ?0,
62
+ subway: ?1,
63
+ rail: ?2,
64
+ bus: ?3,
65
+ ferry: ?4,
66
+ cable_car: ?5,
67
+ gondola: ?6,
68
+ funicular: ?7 )
69
+ col :url
70
+ col :color
71
+ col :text_color
72
+ end
73
+
74
+ col :agency_id
75
+ end
76
+
77
+ file :trips, required: true do
78
+ prefix :trip do
79
+ col :id, required: true, unique: true
80
+ col :headsign
81
+ col :short_name
82
+ col :long_name
83
+ end
84
+
85
+ col :route_id, required: true
86
+ col :service_id, required: true
87
+ col :direction_id,
88
+ &output_map( primary: ?0, opposite: ?1 )
89
+ col :block_id
90
+ col :shape_id
91
+ col :wheelchair_accessible,
92
+ &output_map( :unknown, yes: ?1, no: ?2 )
93
+ col :bikes_allowed,
94
+ &output_map( :unknown, yes: ?1, no: ?2 )
95
+ end
96
+
97
+ file :stop_times, required: true do
98
+ col :trip_id, required: true
99
+ col :arrival_time, required: true
100
+ col :departure_time, required: true
101
+
102
+ prefix :stop do
103
+ col :id, required: true
104
+ col :sequence, required: true
105
+ col :headsign
106
+ end
107
+
108
+ col :pickup_type,
109
+ &output_map( :regular,
110
+ none: ?1,
111
+ phone_agency: ?2,
112
+ coordinate_with_driver: ?3 )
113
+
114
+ col :drop_off_type,
115
+ &output_map( :regular,
116
+ none: ?1,
117
+ phone_agency: ?2,
118
+ coordinate_with_driver: ?3 )
119
+
120
+ col :shape_dist_traveled
121
+ end
122
+
123
+ file :calendar, required: true do
124
+ col :service_id, required: true, unique: true
125
+
126
+ col :monday, required: true, &output_map( yes: ?1, no: ?0 )
127
+ col :tuesday, required: true, &output_map( yes: ?1, no: ?0 )
128
+ col :wednesday, required: true, &output_map( yes: ?1, no: ?0 )
129
+ col :thursday, required: true, &output_map( yes: ?1, no: ?0 )
130
+ col :friday, required: true, &output_map( yes: ?1, no: ?0 )
131
+ col :saturday, required: true, &output_map( yes: ?1, no: ?0 )
132
+ col :sunday, required: true, &output_map( yes: ?1, no: ?0 )
133
+
134
+ col :start_date
135
+ col :end_date
136
+ end
137
+
138
+ file :calendar_dates do
139
+ col :service_id, required: true
140
+ col :date, required: true
141
+ col :exception_type, required: true,
142
+ &output_map( added: ?1, removed: ?2 )
143
+ end
144
+
145
+ file :fare_attributes do
146
+ col :fare_id, required: true, unique: true
147
+ col :price, required: true
148
+ col :currency_type, required: true
149
+ col :payment_method, required: true,
150
+ &output_map( on_board: 0, before: 1 )
151
+ col :transfers, required: true,
152
+ &output_map( :unlimited, none: 0, once: 1, twice: 2 )
153
+ col :transfer_duration
154
+ end
155
+
156
+ file :fare_rules do
157
+ col :fare_id, required: true
158
+ col :route_id
159
+ col :origin_id
160
+ col :destination_id
161
+ col :contains_id
162
+ end
163
+
164
+ file :shapes do
165
+ prefix :shape do
166
+ col :id, required: true
167
+ col :pt_lat, required: true
168
+ col :pt_lon, required: true
169
+ col :pt_sequence, required: true
170
+ col :dist_traveled
171
+ end
172
+ end
173
+
174
+ file :frequencies do
175
+ col :trip_id, required: true
176
+ col :start_time, required: true
177
+ col :end_time, required: true
178
+ col :headway_secs, required: true
179
+ col :exact_times,
180
+ &output_map( :inexact, exact: 1 )
181
+ end
182
+
183
+ file :transfers do
184
+ col :from_stop_id, required: true
185
+ col :to_stop_id, required: true
186
+ col :transfer_type, required: true,
187
+ &output_map( :recommended,
188
+ timed_transfer: 1,
189
+ minimum_time_required: 2,
190
+ impossible: 3 )
191
+ col :min_transfer_time
192
+ end
193
+
194
+ file :feed_info do
195
+ prefix :scope do
196
+ col :publisher_name, required: true
197
+ col :publisher_url, required: true
198
+ col :lang, required: true
199
+ col :start_date
200
+ col :end_date
201
+ col :version
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'file_definition'
2
+
3
+ module GtfsReader
4
+ module Config
5
+ # Describes a GTFS feed and the {FileDefinition files} it is expected to
6
+ # provide.
7
+ class FeedDefinition
8
+ def initialize
9
+ @file_definition = {}
10
+ end
11
+
12
+ #@return [Array<FileDefinition>] All of the defined files.
13
+ def files
14
+ @file_definition.values
15
+ end
16
+
17
+ def required_files
18
+ files.select &:required?
19
+ end
20
+
21
+ def optional_files
22
+ files.reject &:required?
23
+ end
24
+
25
+ #@overload file(name, *args, &block)
26
+ # Defines a new file in the feed.
27
+ #
28
+ # @param name [String] the name of this file within the feed. This name
29
+ # should not include a file extension (like +.txt+)
30
+ # @param args [Array] the first argument is used as a +Hash+ of options
31
+ # to create the new file definition
32
+ # @param block [Proc] this block is +instance_eval+ed on the new
33
+ # {FileDefinition file}
34
+ # @return [FileDefinition] the newly created file
35
+ #
36
+ #@overload file(name)
37
+ # @param name [String] the name of the file to return
38
+ # @return [FileDefinition] the previously created file with the given name
39
+ #@see FileDefinition
40
+ def file(name, *args, &block)
41
+ return @file_definition[name] unless block_given?
42
+
43
+ definition_for!( name, args.first ).tap do |d|
44
+ d.instance_exec &block if block
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def definition_for!(name, opts)
51
+ @file_definition[name] ||= FileDefinition.new( name, opts )
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,111 @@
1
+ require_relative 'column'
2
+ require_relative 'prefixed_column_setter'
3
+
4
+ module GtfsReader
5
+ module Config
6
+ # Describes a single file in a {FeedDefinition GTFS feed}.
7
+ class FileDefinition
8
+ attr_reader :name
9
+
10
+ #@param name [String] The name of the file within the feed.
11
+ #@option opts [Boolean] :required (false)
12
+ # If this file is required to be in the feed.
13
+ def initialize(name, opts={})
14
+ @name, @columns = name, {}
15
+ @opts = { required: false }.merge (opts || {})
16
+ @aliases = {}
17
+ end
18
+
19
+ #@return [Boolean] If this file is required to be in the feed.
20
+ def required?
21
+ @opts[:required]
22
+ end
23
+
24
+ #@return [String] The filename of this file within the GTFS feed.
25
+ def filename
26
+ "#{name}.txt"
27
+ end
28
+
29
+ #@return [Column] The column with the given name
30
+ def [](name)
31
+ @columns[name]
32
+ end
33
+
34
+ def columns
35
+ @columns.values
36
+ end
37
+
38
+ #@return [Array<Column>] The columns required to appear in this file.
39
+ def required_columns
40
+ columns.select &:required?
41
+ end
42
+
43
+ #@return [Array<Column>] The columns not required to appear in this file.
44
+ def optional_columns
45
+ columns.reject &:required?
46
+ end
47
+
48
+ #@return [Array<Column>] The columns which cannot have two rows with the
49
+ # same value.
50
+ def unique_columns
51
+ columns.select &:unique?
52
+ end
53
+
54
+ # Creates a column with the given name.
55
+ #
56
+ #@param name [String] The name of the column to define.
57
+ #@param args [Array] The first element of this args list is used as a
58
+ # +Hash+ of options to create the new column with.
59
+ #@param block [Proc] An optional block used to parse the values of this
60
+ # column on each row.
61
+ #@yieldparam input [String] The value of this column for a particular row.
62
+ #@yieldreturn Any kind of object.
63
+ #@return [Column] The newly created column.
64
+ def col(name, *args, &block)
65
+ name = @aliases[name] if @aliases.key? name
66
+
67
+ if @columns.key? name
68
+ @columns[name].parser &block if block_given?
69
+ return @columns[name]
70
+ end
71
+
72
+ (@columns[name] = Column.new name, args.first, &block).tap do |col|
73
+ @aliases[col.alias] = name if col.alias
74
+ end
75
+ end
76
+
77
+ # Starts a new block within which any defined columns will have the given
78
+ # +sym+ prefixed to its name (joined with an underscore). Also, the
79
+ # defined name given within the block will be aliased to the column.
80
+ #@param sym the prefix to prefixed to each column within the block
81
+ #
82
+ #@example Create a column +route_name+ with the alias +name+
83
+ # prefix( :route ) { name }
84
+ def prefix(sym, &blk)
85
+ PrefixedColumnSetter.new(self, sym.to_s).instance_exec &blk
86
+ end
87
+
88
+ # Creates an input-output proc to convert column values from one form to
89
+ # another.
90
+ #
91
+ # Many parser procs simply map a set of known values to another set of
92
+ # known values. This helper creates such a proc from a given hash and
93
+ # optional default.
94
+ #
95
+ #@param default [] The value to return if there is no mapping for a given
96
+ # input.
97
+ #@param reverse_map [Hash] A map of returns values to their input values.
98
+ # This is in reverse because it looks better, like a list of labels:
99
+ # +{bus: 3, ferry: 4}+
100
+ def output_map(default=nil, reverse_map)
101
+ if reverse_map.values.uniq.length != reverse_map.values.length
102
+ raise FileDefinitionError, "Duplicate values given: #{reverse_map}"
103
+ end
104
+
105
+ map = default.nil? ? {} : Hash.new( default )
106
+ reverse_map.each { |k,v| map[v] = k }
107
+ map.method( :[] ).to_proc
108
+ end
109
+ end
110
+ end
111
+ end