japan_etc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +22 -0
- data/.rubocop_todo.yml +25 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +9 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/generate_database +7 -0
- data/bin/setup +8 -0
- data/database/japan_etc_tollbooths.csv +2701 -0
- data/japan_etc.gemspec +32 -0
- data/lib/japan_etc.rb +6 -0
- data/lib/japan_etc/database.rb +38 -0
- data/lib/japan_etc/database_provider.rb +5 -0
- data/lib/japan_etc/database_provider/base.rb +19 -0
- data/lib/japan_etc/database_provider/central_nexco.rb +98 -0
- data/lib/japan_etc/database_provider/hanshin_expressway.rb +58 -0
- data/lib/japan_etc/database_provider/metropolitan_expressway.rb +81 -0
- data/lib/japan_etc/direction.rb +35 -0
- data/lib/japan_etc/entrance_or_exit.rb +17 -0
- data/lib/japan_etc/error.rb +9 -0
- data/lib/japan_etc/road.rb +46 -0
- data/lib/japan_etc/tollbooth.rb +172 -0
- data/lib/japan_etc/util.rb +30 -0
- data/lib/japan_etc/version.rb +5 -0
- metadata +126 -0
data/japan_etc.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'japan_etc/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'japan_etc'
|
9
|
+
spec.version = JapanETC::VERSION
|
10
|
+
spec.authors = ['Yuji Nakayama']
|
11
|
+
spec.email = ['nkymyj@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Japan ETC (Electronic Toll Collection System) database'
|
14
|
+
spec.description = spec.summary
|
15
|
+
spec.homepage = 'https://github.com/yujinakayama/japan_etc'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = 'exe'
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.add_runtime_dependency 'faraday', '~> 0.15'
|
28
|
+
spec.add_runtime_dependency 'pdf-reader', '~> 2.2'
|
29
|
+
spec.add_runtime_dependency 'spreadsheet', '~> 1.2'
|
30
|
+
|
31
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
32
|
+
end
|
data/lib/japan_etc.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'japan_etc/database_provider'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module JapanETC
|
7
|
+
class Database
|
8
|
+
CSV_HEADER = %i[
|
9
|
+
road_number
|
10
|
+
tollbooth_number
|
11
|
+
road_name
|
12
|
+
route_name
|
13
|
+
tollbooth_name
|
14
|
+
direction
|
15
|
+
entrance_or_exit
|
16
|
+
notes
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
def roads
|
20
|
+
tollbooths.map(&:road).uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
def tollbooths
|
24
|
+
@tollbooths ||= providers.map(&:fetch_tollbooths).flatten.uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
def save_as_csv(filename: 'database/japan_etc_tollbooths.csv')
|
28
|
+
CSV.open(filename, 'w') do |csv|
|
29
|
+
csv << CSV_HEADER
|
30
|
+
tollbooths.each { |tollbooth| csv << tollbooth.to_a }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def providers
|
35
|
+
DatabaseProvider::Base.all.map(&:new)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JapanETC
|
4
|
+
module DatabaseProvider
|
5
|
+
class Base
|
6
|
+
def self.inherited(subclass)
|
7
|
+
all << subclass
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.all
|
11
|
+
@all ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_tollbooths
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'japan_etc/database_provider/base'
|
4
|
+
require 'japan_etc/tollbooth'
|
5
|
+
require 'japan_etc/util'
|
6
|
+
require 'faraday'
|
7
|
+
require 'pdf-reader'
|
8
|
+
|
9
|
+
module JapanETC
|
10
|
+
module DatabaseProvider
|
11
|
+
# https://highwaypost.c-nexco.co.jp/faq/etc/use/50.html
|
12
|
+
class CentralNEXCO < Base
|
13
|
+
include Util
|
14
|
+
|
15
|
+
URL = 'https://highwaypost.c-nexco.co.jp/faq/etc/use/documents/190423-2etcriyoukanouic.pdf'
|
16
|
+
|
17
|
+
WHITESPACE = /[\s ]/.freeze
|
18
|
+
|
19
|
+
TOLLBOOTH_LINE_PATTERN = /
|
20
|
+
\A
|
21
|
+
(?:
|
22
|
+
#{WHITESPACE}{,10}(?<road_name>[^#{WHITESPACE}\d(【][^#{WHITESPACE}]*)#{WHITESPACE}+
|
23
|
+
|
|
24
|
+
#{WHITESPACE}{,10}(?:[(【][^#{WHITESPACE}]+)#{WHITESPACE}+ # Obsolete road name
|
25
|
+
|
|
26
|
+
#{WHITESPACE}{10,}
|
27
|
+
)
|
28
|
+
(?:
|
29
|
+
(?<tollbooth_name>[^#{WHITESPACE}\d(【][^#{WHITESPACE}]*)
|
30
|
+
#{WHITESPACE}+
|
31
|
+
)?
|
32
|
+
(?<identifiers>\d{2}#{WHITESPACE}+\d{3}\b.*?)
|
33
|
+
(?:
|
34
|
+
※
|
35
|
+
(?<note>.+?)
|
36
|
+
#{WHITESPACE}*
|
37
|
+
)?
|
38
|
+
\z
|
39
|
+
/x.freeze
|
40
|
+
|
41
|
+
IDENTIFIER_PATTERN = /\b(\d{2})#{WHITESPACE}+(\d{3})\b/.freeze
|
42
|
+
|
43
|
+
attr_reader :current_road_name, :current_route_name, :current_tollbooth_name
|
44
|
+
|
45
|
+
def fetch_tollbooths
|
46
|
+
lines.flat_map { |line| parse_line(line) }.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_line(line)
|
50
|
+
match = line.match(TOLLBOOTH_LINE_PATTERN)
|
51
|
+
return unless match
|
52
|
+
|
53
|
+
if match[:road_name]
|
54
|
+
@current_road_name, @current_route_name =
|
55
|
+
extract_route_name_from_road_name(match[:road_name])
|
56
|
+
@current_road_name = canonicalize(@current_road_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
@current_tollbooth_name = match[:tollbooth_name] if match[:tollbooth_name]
|
60
|
+
|
61
|
+
identifiers = match[:identifiers].scan(IDENTIFIER_PATTERN)
|
62
|
+
|
63
|
+
identifiers.map do |identifier|
|
64
|
+
Tollbooth.create(
|
65
|
+
road_number: identifier.first,
|
66
|
+
tollbooth_number: identifier.last,
|
67
|
+
road_name: current_road_name,
|
68
|
+
route_name: current_route_name,
|
69
|
+
name: current_tollbooth_name,
|
70
|
+
note: match[:note]
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def extract_route_name_from_road_name(road_name)
|
76
|
+
road_name = normalize(road_name)
|
77
|
+
match = road_name.match(/\A(?<road_name>.+?)(?<route_name>\d+号.+)?\z/)
|
78
|
+
road_name = match[:road_name].sub(/高速\z/, '高速道路')
|
79
|
+
[road_name, match[:route_name]]
|
80
|
+
end
|
81
|
+
|
82
|
+
def canonicalize(road_name)
|
83
|
+
road_name = '首都圏中央連絡自動車道' if road_name == '首都圏中央連絡道'
|
84
|
+
road_name = road_name.sub(/高速\z/, '高速道路')
|
85
|
+
road_name
|
86
|
+
end
|
87
|
+
|
88
|
+
def lines
|
89
|
+
pdf.pages.flat_map { |page| page.text.each_line.map(&:chomp).to_a }
|
90
|
+
end
|
91
|
+
|
92
|
+
def pdf
|
93
|
+
response = Faraday.get(URL)
|
94
|
+
PDF::Reader.new(StringIO.new(response.body))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'japan_etc/database_provider/base'
|
4
|
+
require 'japan_etc/tollbooth'
|
5
|
+
require 'faraday'
|
6
|
+
require 'spreadsheet'
|
7
|
+
|
8
|
+
module JapanETC
|
9
|
+
module DatabaseProvider
|
10
|
+
# https://www.hanshin-exp.co.jp/drivers/ryoukin/etc_ryokinsyo/
|
11
|
+
class HanshinExpressway < Base
|
12
|
+
URL = 'https://www.hanshin-exp.co.jp/drivers/ryoukin/files/code_20170516.xls'
|
13
|
+
|
14
|
+
def fetch_tollbooths
|
15
|
+
rows.flat_map do |row|
|
16
|
+
process_row(row)
|
17
|
+
end.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
def process_row(row)
|
21
|
+
route_name, road_number, tollbooth_number, tollbooth_name, _, note = row
|
22
|
+
|
23
|
+
return nil if !road_number.is_a?(Numeric) || !tollbooth_number.is_a?(Numeric)
|
24
|
+
|
25
|
+
tollbooth = Tollbooth.create(
|
26
|
+
road_number: road_number,
|
27
|
+
tollbooth_number: tollbooth_number,
|
28
|
+
road_name: '阪神高速道路',
|
29
|
+
route_name: route_name,
|
30
|
+
name: tollbooth_name,
|
31
|
+
note: note
|
32
|
+
)
|
33
|
+
|
34
|
+
remove_redundant_name_suffix!(tollbooth)
|
35
|
+
|
36
|
+
tollbooth
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_redundant_name_suffix!(tollbooth)
|
40
|
+
return unless tollbooth.entrance_or_exit
|
41
|
+
|
42
|
+
tollbooth.name.sub!(/[入出]\z/) do |match|
|
43
|
+
found_entrance_or_exit = EntranceOrExit.from(match)
|
44
|
+
found_entrance_or_exit == tollbooth.entrance_or_exit ? '' : match
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def rows
|
49
|
+
workbook.worksheets.first.rows
|
50
|
+
end
|
51
|
+
|
52
|
+
def workbook
|
53
|
+
response = Faraday.get(URL)
|
54
|
+
Spreadsheet.open(StringIO.new(response.body))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'japan_etc/database_provider/base'
|
4
|
+
require 'japan_etc/tollbooth'
|
5
|
+
require 'csv'
|
6
|
+
require 'faraday'
|
7
|
+
|
8
|
+
module JapanETC
|
9
|
+
module DatabaseProvider
|
10
|
+
# https://www.shutoko.jp/fee/tollbooth/
|
11
|
+
class MetropolitanExpressway < Base
|
12
|
+
URL = 'https://www.shutoko.jp/fee/tollbooth/~/media/pdf/customer/fee/tollbooth/code190201.csv/'
|
13
|
+
|
14
|
+
OPPOSITE_DIRECTIONS = {
|
15
|
+
'上' => '下',
|
16
|
+
'下' => '上',
|
17
|
+
'外' => '内',
|
18
|
+
'内' => '外',
|
19
|
+
'東' => '西',
|
20
|
+
'西' => '東'
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
DIRECTION_SUFFIX_PATTERN = /[#{OPPOSITE_DIRECTIONS.keys.join('')}]\z/.freeze
|
24
|
+
|
25
|
+
def fetch_tollbooths
|
26
|
+
original_tollbooths.map do |original_tollbooth|
|
27
|
+
tollbooth = original_tollbooth.dup
|
28
|
+
extract_direction_from_name!(tollbooth)
|
29
|
+
tollbooth
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_direction_from_name!(tollbooth)
|
34
|
+
match = tollbooth.name.match(DIRECTION_SUFFIX_PATTERN)
|
35
|
+
|
36
|
+
return unless match
|
37
|
+
|
38
|
+
direction = match.to_s
|
39
|
+
|
40
|
+
return if %w[東 西].include?(direction) && tollbooth.road.route_name != '湾岸線'
|
41
|
+
|
42
|
+
opposite_name = tollbooth.name.sub(DIRECTION_SUFFIX_PATTERN, OPPOSITE_DIRECTIONS[direction])
|
43
|
+
|
44
|
+
opposite_tollbooth_exists = original_tollbooths.find do |other_tollbooth|
|
45
|
+
other_tollbooth.road == tollbooth.road && other_tollbooth.name == opposite_name
|
46
|
+
end
|
47
|
+
|
48
|
+
return unless opposite_tollbooth_exists
|
49
|
+
|
50
|
+
tollbooth.direction = Direction.from(direction)
|
51
|
+
tollbooth.name.sub!(DIRECTION_SUFFIX_PATTERN, '')
|
52
|
+
end
|
53
|
+
|
54
|
+
def original_tollbooths
|
55
|
+
@original_tollbooths ||= rows.map do |row|
|
56
|
+
Tollbooth.create(
|
57
|
+
road_number: row[0],
|
58
|
+
tollbooth_number: row[1],
|
59
|
+
road_name: '首都高速道路',
|
60
|
+
route_name: row[2],
|
61
|
+
name: row[3],
|
62
|
+
entrance_or_exit: EntranceOrExit.from(row[4])
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def rows
|
68
|
+
CSV.parse(csv, headers: :first_row)
|
69
|
+
end
|
70
|
+
|
71
|
+
def csv
|
72
|
+
shiftjis_csv.encode(Encoding::UTF_8)
|
73
|
+
end
|
74
|
+
|
75
|
+
def shiftjis_csv
|
76
|
+
response = Faraday.get(URL)
|
77
|
+
response.body.force_encoding(Encoding::Shift_JIS)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JapanETC
|
4
|
+
module Direction
|
5
|
+
INBOUND = '上り'
|
6
|
+
OUTBOUND = '下り'
|
7
|
+
CLOCKWISE = '外回り'
|
8
|
+
COUNTERCLOCKWISE = '内回り'
|
9
|
+
NORTH = '北行き'
|
10
|
+
SOUTH = '南行き'
|
11
|
+
EAST = '東行き'
|
12
|
+
WEST = '西行き'
|
13
|
+
|
14
|
+
def self.from(text)
|
15
|
+
case text
|
16
|
+
when '上り', '上'
|
17
|
+
INBOUND
|
18
|
+
when '下り', '下'
|
19
|
+
OUTBOUND
|
20
|
+
when '外回り', '外'
|
21
|
+
CLOCKWISE
|
22
|
+
when '内回り', '内'
|
23
|
+
COUNTERCLOCKWISE
|
24
|
+
when /北行/, '北'
|
25
|
+
NORTH
|
26
|
+
when /南行/, '南'
|
27
|
+
SOUTH
|
28
|
+
when /東行/, '東'
|
29
|
+
EAST
|
30
|
+
when /西行/, '西'
|
31
|
+
WEST
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'japan_etc/error'
|
4
|
+
require 'japan_etc/util'
|
5
|
+
|
6
|
+
module JapanETC
|
7
|
+
Road = Struct.new(:name, :route_name) do
|
8
|
+
include Util
|
9
|
+
|
10
|
+
IRREGULAR_ABBREVIATIONS = {
|
11
|
+
'首都圏中央連絡自動車道' => '圏央道',
|
12
|
+
'名古屋第二環状自動車道' => '名二環'
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(name, route_name = nil)
|
16
|
+
raise ValidationError, '#name cannot be nil' if name.nil?
|
17
|
+
|
18
|
+
super(normalize(name), normalize(route_name))
|
19
|
+
end
|
20
|
+
|
21
|
+
def abbreviation
|
22
|
+
@abbreviation ||=
|
23
|
+
if (irregular_abbreviation = IRREGULAR_ABBREVIATIONS[name])
|
24
|
+
irregular_abbreviation
|
25
|
+
else
|
26
|
+
regular_abbreviation
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def regular_abbreviation
|
31
|
+
abbreviation = name.dup
|
32
|
+
|
33
|
+
if abbreviation.start_with?('第')
|
34
|
+
abbreviation = abbreviation.sub(/高速道路|自動車道|道路/, '')
|
35
|
+
end
|
36
|
+
|
37
|
+
abbreviation = abbreviation
|
38
|
+
.sub('高速道路', '高速')
|
39
|
+
.sub('自動車道', '道')
|
40
|
+
.sub('道路', '道')
|
41
|
+
.sub('有料', '')
|
42
|
+
|
43
|
+
abbreviation
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|