japan_etc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'japan_etc/version'
4
+
5
+ module JapanETC
6
+ end
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'japan_etc/database_provider/metropolitan_expressway'
4
+ require 'japan_etc/database_provider/hanshin_expressway'
5
+ require 'japan_etc/database_provider/central_nexco'
@@ -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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JapanETC
4
+ module EntranceOrExit
5
+ ENTRANCE = '入口'
6
+ EXIT = '出口'
7
+
8
+ def self.from(text)
9
+ case text
10
+ when /入口/, /(入)/, '入'
11
+ ENTRANCE
12
+ when /出口/, /(出)/, '出'
13
+ EXIT
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JapanETC
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ValidationError < Error
8
+ end
9
+ 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