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