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