jp_address_complement 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/.agent/rules/specify-rules.md +29 -0
- data/.agent/workflows/speckit.analyze.md +184 -0
- data/.agent/workflows/speckit.checklist.md +294 -0
- data/.agent/workflows/speckit.clarify.md +181 -0
- data/.agent/workflows/speckit.constitution.md +84 -0
- data/.agent/workflows/speckit.implement.md +135 -0
- data/.agent/workflows/speckit.plan.md +90 -0
- data/.agent/workflows/speckit.specify.md +258 -0
- data/.agent/workflows/speckit.tasks.md +137 -0
- data/.agent/workflows/speckit.taskstoissues.md +30 -0
- data/.claude/commands/speckit.analyze.md +184 -0
- data/.claude/commands/speckit.checklist.md +294 -0
- data/.claude/commands/speckit.clarify.md +181 -0
- data/.claude/commands/speckit.constitution.md +84 -0
- data/.claude/commands/speckit.implement.md +135 -0
- data/.claude/commands/speckit.plan.md +90 -0
- data/.claude/commands/speckit.specify.md +258 -0
- data/.claude/commands/speckit.tasks.md +137 -0
- data/.claude/commands/speckit.taskstoissues.md +30 -0
- data/.cursor/commands/speckit.analyze.md +184 -0
- data/.cursor/commands/speckit.checklist.md +294 -0
- data/.cursor/commands/speckit.clarify.md +181 -0
- data/.cursor/commands/speckit.constitution.md +84 -0
- data/.cursor/commands/speckit.implement.md +135 -0
- data/.cursor/commands/speckit.plan.md +90 -0
- data/.cursor/commands/speckit.specify.md +258 -0
- data/.cursor/commands/speckit.tasks.md +137 -0
- data/.cursor/commands/speckit.taskstoissues.md +30 -0
- data/.cursor/rules/specify-rules.mdc +32 -0
- data/.rubocop.yml +118 -0
- data/.specify/memory/constitution.md +130 -0
- data/.specify/scripts/bash/check-prerequisites.sh +166 -0
- data/.specify/scripts/bash/common.sh +156 -0
- data/.specify/scripts/bash/create-new-feature.sh +297 -0
- data/.specify/scripts/bash/setup-plan.sh +61 -0
- data/.specify/scripts/bash/update-agent-context.sh +810 -0
- data/.specify/templates/agent-file-template.md +28 -0
- data/.specify/templates/checklist-template.md +40 -0
- data/.specify/templates/constitution-template.md +50 -0
- data/.specify/templates/plan-template.md +104 -0
- data/.specify/templates/spec-template.md +115 -0
- data/.specify/templates/tasks-template.md +251 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE +9 -0
- data/README.md +274 -0
- data/Rakefile +19 -0
- data/Steepfile +9 -0
- data/examples/rails/jp_address_complement_demo/.gitignore +15 -0
- data/examples/rails/jp_address_complement_demo/.ruby-version +1 -0
- data/examples/rails/jp_address_complement_demo/Gemfile +22 -0
- data/examples/rails/jp_address_complement_demo/Gemfile.lock +252 -0
- data/examples/rails/jp_address_complement_demo/README.md +57 -0
- data/examples/rails/jp_address_complement_demo/Rakefile +6 -0
- data/examples/rails/jp_address_complement_demo/app/assets/images/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/app/assets/stylesheets/application.css +1 -0
- data/examples/rails/jp_address_complement_demo/app/controllers/addresses_controller.rb +59 -0
- data/examples/rails/jp_address_complement_demo/app/controllers/application_controller.rb +4 -0
- data/examples/rails/jp_address_complement_demo/app/controllers/concerns/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/app/helpers/application_helper.rb +2 -0
- data/examples/rails/jp_address_complement_demo/app/models/application_record.rb +3 -0
- data/examples/rails/jp_address_complement_demo/app/models/concerns/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/app/views/addresses/index.html.erb +22 -0
- data/examples/rails/jp_address_complement_demo/app/views/addresses/prefecture.html.erb +20 -0
- data/examples/rails/jp_address_complement_demo/app/views/addresses/prefix.html.erb +25 -0
- data/examples/rails/jp_address_complement_demo/app/views/addresses/reverse.html.erb +30 -0
- data/examples/rails/jp_address_complement_demo/app/views/addresses/validate.html.erb +23 -0
- data/examples/rails/jp_address_complement_demo/app/views/layouts/application.html.erb +40 -0
- data/examples/rails/jp_address_complement_demo/app/views/pwa/manifest.json.erb +22 -0
- data/examples/rails/jp_address_complement_demo/app/views/pwa/service-worker.js +26 -0
- data/examples/rails/jp_address_complement_demo/bin/ci +6 -0
- data/examples/rails/jp_address_complement_demo/bin/dev +2 -0
- data/examples/rails/jp_address_complement_demo/bin/rails +4 -0
- data/examples/rails/jp_address_complement_demo/bin/rake +4 -0
- data/examples/rails/jp_address_complement_demo/bin/setup +35 -0
- data/examples/rails/jp_address_complement_demo/config/application.rb +42 -0
- data/examples/rails/jp_address_complement_demo/config/boot.rb +3 -0
- data/examples/rails/jp_address_complement_demo/config/ci.rb +15 -0
- data/examples/rails/jp_address_complement_demo/config/credentials.yml.enc +1 -0
- data/examples/rails/jp_address_complement_demo/config/database.yml +31 -0
- data/examples/rails/jp_address_complement_demo/config/environment.rb +5 -0
- data/examples/rails/jp_address_complement_demo/config/environments/development.rb +54 -0
- data/examples/rails/jp_address_complement_demo/config/environments/production.rb +67 -0
- data/examples/rails/jp_address_complement_demo/config/environments/test.rb +42 -0
- data/examples/rails/jp_address_complement_demo/config/initializers/content_security_policy.rb +29 -0
- data/examples/rails/jp_address_complement_demo/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/rails/jp_address_complement_demo/config/initializers/inflections.rb +16 -0
- data/examples/rails/jp_address_complement_demo/config/locales/en.yml +31 -0
- data/examples/rails/jp_address_complement_demo/config/puma.rb +39 -0
- data/examples/rails/jp_address_complement_demo/config/routes.rb +19 -0
- data/examples/rails/jp_address_complement_demo/config.ru +6 -0
- data/examples/rails/jp_address_complement_demo/db/migrate/20260228083709_create_jp_address_complement_postal_codes.rb +44 -0
- data/examples/rails/jp_address_complement_demo/db/schema.rb +33 -0
- data/examples/rails/jp_address_complement_demo/db/seeds.rb +24 -0
- data/examples/rails/jp_address_complement_demo/lib/tasks/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/log/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/public/400.html +135 -0
- data/examples/rails/jp_address_complement_demo/public/404.html +135 -0
- data/examples/rails/jp_address_complement_demo/public/406-unsupported-browser.html +135 -0
- data/examples/rails/jp_address_complement_demo/public/422.html +135 -0
- data/examples/rails/jp_address_complement_demo/public/500.html +135 -0
- data/examples/rails/jp_address_complement_demo/public/icon.png +0 -0
- data/examples/rails/jp_address_complement_demo/public/icon.svg +3 -0
- data/examples/rails/jp_address_complement_demo/public/robots.txt +1 -0
- data/examples/rails/jp_address_complement_demo/script/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/storage/.keep +0 -0
- data/examples/rails/jp_address_complement_demo/vendor/.keep +0 -0
- data/lib/generators/jp_address_complement/install_generator.rb +34 -0
- data/lib/generators/jp_address_complement/templates/create_jp_address_complement_postal_codes.rb.erb +45 -0
- data/lib/jp_address_complement/address_record.rb +36 -0
- data/lib/jp_address_complement/configuration.rb +21 -0
- data/lib/jp_address_complement/importers/csv_importer.rb +148 -0
- data/lib/jp_address_complement/ken_all_downloader.rb +122 -0
- data/lib/jp_address_complement/models/postal_code.rb +21 -0
- data/lib/jp_address_complement/normalizer.rb +77 -0
- data/lib/jp_address_complement/prefecture.rb +105 -0
- data/lib/jp_address_complement/railtie.rb +27 -0
- data/lib/jp_address_complement/repositories/active_record_postal_code_repository.rb +78 -0
- data/lib/jp_address_complement/repositories/csv_postal_code_repository.rb +200 -0
- data/lib/jp_address_complement/repositories/postal_code_repository.rb +36 -0
- data/lib/jp_address_complement/searcher.rb +85 -0
- data/lib/jp_address_complement/validators/address_validator.rb +41 -0
- data/lib/jp_address_complement/version.rb +6 -0
- data/lib/jp_address_complement.rb +129 -0
- data/lib/tasks/jp_address_complement.rake +32 -0
- data/rbs_collection.lock.yaml +380 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/generators/jp_address_complement/install_generator.rbs +18 -0
- data/sig/generated/jp_address_complement/configuration.rbs +18 -0
- data/sig/generated/jp_address_complement/importers/csv_importer.rbs +78 -0
- data/sig/generated/jp_address_complement/ken_all_downloader.rbs +49 -0
- data/sig/generated/jp_address_complement/normalizer.rbs +37 -0
- data/sig/generated/jp_address_complement/prefecture.rbs +27 -0
- data/sig/generated/jp_address_complement/railtie.rbs +8 -0
- data/sig/generated/jp_address_complement/repositories/active_record_postal_code_repository.rbs +38 -0
- data/sig/generated/jp_address_complement/repositories/csv_postal_code_repository.rbs +100 -0
- data/sig/generated/jp_address_complement/repositories/postal_code_repository.rbs +29 -0
- data/sig/generated/jp_address_complement/searcher.rbs +43 -0
- data/sig/generated/jp_address_complement/validators/address_validator.rbs +24 -0
- data/sig/generated/jp_address_complement/version.rbs +5 -0
- data/sig/generated/jp_address_complement.rbs +84 -0
- data/sig/manual/address_record.rbs +40 -0
- data/sig/manual/gem_rubyzip.rbs +17 -0
- data/sig/manual/postal_code.rbs +9 -0
- data/sig/manual/stdlib_csv_invalid_encoding_error.rbs +5 -0
- data/sig/manual/stdlib_net_http.rbs +33 -0
- data/sig/manual/stdlib_openuri.rbs +9 -0
- data/sig/manual/stdlib_tmpdir.rbs +4 -0
- data/specs/001-jp-address-complement-gem/checklists/requirements.md +36 -0
- data/specs/001-jp-address-complement-gem/contracts/public-api.md +209 -0
- data/specs/001-jp-address-complement-gem/data-model.md +207 -0
- data/specs/001-jp-address-complement-gem/plan.md +124 -0
- data/specs/001-jp-address-complement-gem/quickstart.md +151 -0
- data/specs/001-jp-address-complement-gem/research.md +139 -0
- data/specs/001-jp-address-complement-gem/spec.md +153 -0
- data/specs/001-jp-address-complement-gem/tasks.md +279 -0
- data/specs/002-rbs-type-annotations/checklists/requirements.md +37 -0
- data/specs/002-rbs-type-annotations/contracts/rbs-public-api.md +116 -0
- data/specs/002-rbs-type-annotations/data-model.md +119 -0
- data/specs/002-rbs-type-annotations/plan.md +116 -0
- data/specs/002-rbs-type-annotations/quickstart.md +105 -0
- data/specs/002-rbs-type-annotations/research.md +173 -0
- data/specs/002-rbs-type-annotations/spec.md +125 -0
- data/specs/002-rbs-type-annotations/tasks.md +189 -0
- data/specs/003-csv-remove-obsolete/checklists/requirements.md +34 -0
- data/specs/003-csv-remove-obsolete/contracts/csv-import.md +41 -0
- data/specs/003-csv-remove-obsolete/data-model.md +47 -0
- data/specs/003-csv-remove-obsolete/plan.md +73 -0
- data/specs/003-csv-remove-obsolete/quickstart.md +40 -0
- data/specs/003-csv-remove-obsolete/research.md +71 -0
- data/specs/003-csv-remove-obsolete/spec.md +85 -0
- data/specs/003-csv-remove-obsolete/tasks.md +167 -0
- data/specs/004-prefecture-code-reverse-lookup/checklists/requirements.md +34 -0
- data/specs/004-prefecture-code-reverse-lookup/contracts/public-api-prefecture-and-reverse.md +122 -0
- data/specs/004-prefecture-code-reverse-lookup/data-model.md +81 -0
- data/specs/004-prefecture-code-reverse-lookup/plan.md +92 -0
- data/specs/004-prefecture-code-reverse-lookup/quickstart.md +91 -0
- data/specs/004-prefecture-code-reverse-lookup/research.md +62 -0
- data/specs/004-prefecture-code-reverse-lookup/spec.md +120 -0
- data/specs/004-prefecture-code-reverse-lookup/tasks.md +190 -0
- metadata +451 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
require 'tmpdir'
|
|
8
|
+
require 'uri'
|
|
9
|
+
require 'zip'
|
|
10
|
+
|
|
11
|
+
module JpAddressComplement
|
|
12
|
+
# 郵便番号データ(utf_ken_all.zip)を公式URLからダウンロードし、展開して CSV のパスを返す
|
|
13
|
+
class KenAllDownloader
|
|
14
|
+
# @rbs String
|
|
15
|
+
DEFAULT_URL = 'https://www.post.japanpost.jp/zipcode/dl/utf/zip/utf_ken_all.zip'
|
|
16
|
+
# @rbs String
|
|
17
|
+
CSV_FILENAME = 'utf_ken_all.csv'
|
|
18
|
+
|
|
19
|
+
class DownloadError < JpAddressComplement::Error; end
|
|
20
|
+
|
|
21
|
+
# @rbs (?String url) -> void
|
|
22
|
+
# @param url [String] ダウンロードする zip の URL(省略時は DEFAULT_URL)
|
|
23
|
+
def initialize(url = DEFAULT_URL)
|
|
24
|
+
@url = url
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# zip をダウンロードして一時ディレクトリに展開し、KEN_ALL.CSV の絶対パスを返す。
|
|
28
|
+
# 呼び出し元でインポート完了まで一時ディレクトリは削除されない(プロセス終了時に OS が削除)。
|
|
29
|
+
# @rbs () -> String
|
|
30
|
+
# @return [String] 展開された KEN_ALL.CSV の絶対パス
|
|
31
|
+
# @raise [DownloadError] ダウンロード・展開・ファイルが見つからない場合
|
|
32
|
+
def download_and_extract
|
|
33
|
+
zip_path = download_zip
|
|
34
|
+
extract_zip(zip_path)
|
|
35
|
+
ensure
|
|
36
|
+
File.unlink(zip_path) if zip_path && File.exist?(zip_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @rbs () -> String
|
|
42
|
+
def download_zip
|
|
43
|
+
tmp = Tempfile.new(['ken_all', '.zip'])
|
|
44
|
+
tmp.binmode
|
|
45
|
+
tmp.close
|
|
46
|
+
path = tmp.path or raise 'Tempfile#path is nil'
|
|
47
|
+
fetch_to_path(path)
|
|
48
|
+
path
|
|
49
|
+
rescue SocketError, Timeout::Error, Errno::ENOENT => e
|
|
50
|
+
raise DownloadError, "ダウンロードに失敗しました: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @rbs (String path) -> void
|
|
54
|
+
def fetch_to_path(path)
|
|
55
|
+
uri = URI.parse(@url)
|
|
56
|
+
host = uri.host or raise DownloadError, "URL に host がありません: #{@url}"
|
|
57
|
+
port = uri.port || (uri.scheme == 'https' ? 443 : 80)
|
|
58
|
+
use_ssl = uri.scheme == 'https'
|
|
59
|
+
Net::HTTP.start(host, port, use_ssl: use_ssl, read_timeout: 60, open_timeout: 10) do |http|
|
|
60
|
+
request = Net::HTTP::Get.new(uri)
|
|
61
|
+
http.request(request) { |response| write_response_to_path(response, path) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @rbs (Net::HTTPResponse response, String path) -> void
|
|
66
|
+
def write_response_to_path(response, path)
|
|
67
|
+
raise DownloadError, "HTTP error: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
68
|
+
|
|
69
|
+
File.open(path, 'wb') do |f|
|
|
70
|
+
if response.respond_to?(:body_io)
|
|
71
|
+
IO.copy_stream(response.body_io, f)
|
|
72
|
+
else
|
|
73
|
+
# Net::HTTPResponse#body は stdlib RBS で型が不足することがある
|
|
74
|
+
body = response.body # steep:ignore
|
|
75
|
+
f.write(body)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @rbs (String zip_path) -> String
|
|
81
|
+
def extract_zip(zip_path)
|
|
82
|
+
tmpdir = Dir.mktmpdir('jp_address_complement_ken_all')
|
|
83
|
+
tmpdir = File.expand_path(tmpdir) # 相対の場合は絶対パスに正規化(realpath はシンボリックリンク解決で別パスになるため使わない)
|
|
84
|
+
csv_path = extract_zip_entries(zip_path, tmpdir)
|
|
85
|
+
csv_path ||= find_csv_in_dir(tmpdir)
|
|
86
|
+
raise DownloadError, "ZIP 内に #{CSV_FILENAME} が見つかりません" if csv_path.nil?
|
|
87
|
+
|
|
88
|
+
csv_path
|
|
89
|
+
rescue Zip::Error => e
|
|
90
|
+
raise DownloadError, "ZIP の展開に失敗しました: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @rbs (String zip_path, String tmpdir) -> String?
|
|
94
|
+
def extract_zip_entries(zip_path, tmpdir)
|
|
95
|
+
csv_path = nil
|
|
96
|
+
Zip::File.open(zip_path) do |zip_file|
|
|
97
|
+
Dir.chdir(tmpdir) do
|
|
98
|
+
zip_file.each do |entry|
|
|
99
|
+
name = entry.name.delete_prefix('/')
|
|
100
|
+
dir = File.dirname(name)
|
|
101
|
+
FileUtils.mkdir_p(dir) unless dir == '.'
|
|
102
|
+
# RubyZip::Entry#extract(dest_path = nil) を使い、既存ファイルがあればブロックで上書き許可する。
|
|
103
|
+
entry.extract(name) { true } # steep:ignore
|
|
104
|
+
dest = File.join(tmpdir, name)
|
|
105
|
+
csv_path = dest if entry_csv?(name, dest)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
csv_path
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @rbs (String entry_name, String dest) -> bool
|
|
113
|
+
def entry_csv?(entry_name, dest)
|
|
114
|
+
File.basename(entry_name) == CSV_FILENAME && File.file?(dest)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @rbs (String tmpdir) -> String?
|
|
118
|
+
def find_csv_in_dir(tmpdir)
|
|
119
|
+
Dir.glob(File.join(tmpdir, '**', CSV_FILENAME)).find { |p| File.file?(p) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: disabled — 継承元が base_record_class で可変のため型は sig/manual/ で手動管理
|
|
4
|
+
require 'active_record'
|
|
5
|
+
|
|
6
|
+
module JpAddressComplement
|
|
7
|
+
# 住所テーブルに対応する ActiveRecord モデル
|
|
8
|
+
# 継承元は JpAddressComplement.base_record_class(未設定時は ActiveRecord::Base)
|
|
9
|
+
# テーブル名は JpAddressComplement.postal_code_table_name(未設定時は 'jp_address_complement_postal_codes')。initializer で変更可能。
|
|
10
|
+
class PostalCode < base_record_class
|
|
11
|
+
# initializer がモデル読み込み後に実行されるため、参照のたびに設定を読む
|
|
12
|
+
def self.table_name
|
|
13
|
+
JpAddressComplement.postal_code_table_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
validates :postal_code, presence: true, format: { with: /\A\d{7}\z/ }
|
|
17
|
+
validates :pref_code, presence: true
|
|
18
|
+
validates :pref, presence: true
|
|
19
|
+
validates :city, presence: true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module JpAddressComplement
|
|
5
|
+
# 郵便番号の正規化処理を担当するクラス
|
|
6
|
+
# 全角→半角変換・〒記号除去・ハイフン除去を行う
|
|
7
|
+
class Normalizer
|
|
8
|
+
# 半角数字以外の文字を除去するためのパターン
|
|
9
|
+
DIGIT_ONLY = /\A\d{7}\z/ #: Regexp
|
|
10
|
+
PREFIX_MIN_LENGTH = 4 #: Integer
|
|
11
|
+
|
|
12
|
+
# 町域から除去する「通常住所に含まれない」固定文字列(漢字・かな両方)
|
|
13
|
+
TOWN_DISPLAY_REMOVAL_STRINGS = [
|
|
14
|
+
'以下に掲載がない場合',
|
|
15
|
+
'イカニケイサイガナイバアイ' # 以下に掲載がない場合(カナ)
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# 郵便番号文字列を正規化して7桁の半角数字文字列を返す
|
|
20
|
+
# @rbs (String?) -> String?
|
|
21
|
+
# @param code [String, nil] 郵便番号文字列(ハイフン・全角・〒記号を自動除去)
|
|
22
|
+
# @return [String, nil] 正規化後の7桁郵便番号。不正な場合は nil
|
|
23
|
+
def normalize_postal_code(code)
|
|
24
|
+
return nil if code.blank?
|
|
25
|
+
|
|
26
|
+
normalized = normalize_string(code)
|
|
27
|
+
return nil unless normalized.match?(DIGIT_ONLY)
|
|
28
|
+
|
|
29
|
+
normalized
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 郵便番号プレフィックスを正規化する
|
|
33
|
+
# @rbs (String?) -> String?
|
|
34
|
+
# @param prefix [String, nil] 郵便番号の先頭部分(4桁以上)
|
|
35
|
+
# @return [String, nil] 正規化後の数字文字列。4桁未満または不正な場合は nil
|
|
36
|
+
def normalize_prefix(prefix)
|
|
37
|
+
return nil if prefix.blank?
|
|
38
|
+
|
|
39
|
+
normalized = normalize_string(prefix)
|
|
40
|
+
return nil if normalized.empty?
|
|
41
|
+
return nil unless normalized.match?(/\A\d+\z/)
|
|
42
|
+
return nil if normalized.length < PREFIX_MIN_LENGTH
|
|
43
|
+
|
|
44
|
+
normalized
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 町域文字列から「通常住所に含まれない情報」を除いた表示用文字列を返す
|
|
48
|
+
# 除去対象: 「以下に掲載がない場合」、全角括弧()で囲まれた部分全体
|
|
49
|
+
# @rbs (String?) -> String?
|
|
50
|
+
# @param town_str [String, nil] 町域(漢字)または町域カナ
|
|
51
|
+
# @return [String, nil] 除去後の文字列。nil または空になった場合は nil
|
|
52
|
+
def normalize_town_for_display(town_str)
|
|
53
|
+
return nil if town_str.nil?
|
|
54
|
+
|
|
55
|
+
s = town_str.to_s.strip
|
|
56
|
+
return nil if s.empty?
|
|
57
|
+
|
|
58
|
+
# 全角括弧()で囲まれた部分をすべて除去
|
|
59
|
+
s = s.gsub(/([^)]*)/, '')
|
|
60
|
+
TOWN_DISPLAY_REMOVAL_STRINGS.each { |rem| s = s.gsub(rem, '') }
|
|
61
|
+
s = s.strip
|
|
62
|
+
s.empty? ? nil : s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @rbs (String) -> String
|
|
68
|
+
def normalize_string(str)
|
|
69
|
+
str
|
|
70
|
+
.tr('〒', '')
|
|
71
|
+
.tr('0-9', '0-9')
|
|
72
|
+
.tr('-ー-', '')
|
|
73
|
+
.strip
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module JpAddressComplement
|
|
5
|
+
# JIS X 0401 都道府県コード(01–47)⇔都道府県名の変換
|
|
6
|
+
module Prefecture
|
|
7
|
+
# JIS X 0401 都道府県コード → 都道府県名(正式名称)
|
|
8
|
+
CODE_TO_NAME = {
|
|
9
|
+
'01' => '北海道',
|
|
10
|
+
'02' => '青森県',
|
|
11
|
+
'03' => '岩手県',
|
|
12
|
+
'04' => '宮城県',
|
|
13
|
+
'05' => '秋田県',
|
|
14
|
+
'06' => '山形県',
|
|
15
|
+
'07' => '福島県',
|
|
16
|
+
'08' => '茨城県',
|
|
17
|
+
'09' => '栃木県',
|
|
18
|
+
'10' => '群馬県',
|
|
19
|
+
'11' => '埼玉県',
|
|
20
|
+
'12' => '千葉県',
|
|
21
|
+
'13' => '東京都',
|
|
22
|
+
'14' => '神奈川県',
|
|
23
|
+
'15' => '新潟県',
|
|
24
|
+
'16' => '富山県',
|
|
25
|
+
'17' => '石川県',
|
|
26
|
+
'18' => '福井県',
|
|
27
|
+
'19' => '山梨県',
|
|
28
|
+
'20' => '長野県',
|
|
29
|
+
'21' => '岐阜県',
|
|
30
|
+
'22' => '静岡県',
|
|
31
|
+
'23' => '愛知県',
|
|
32
|
+
'24' => '三重県',
|
|
33
|
+
'25' => '滋賀県',
|
|
34
|
+
'26' => '京都府',
|
|
35
|
+
'27' => '大阪府',
|
|
36
|
+
'28' => '兵庫県',
|
|
37
|
+
'29' => '奈良県',
|
|
38
|
+
'30' => '和歌山県',
|
|
39
|
+
'31' => '鳥取県',
|
|
40
|
+
'32' => '島根県',
|
|
41
|
+
'33' => '岡山県',
|
|
42
|
+
'34' => '広島県',
|
|
43
|
+
'35' => '山口県',
|
|
44
|
+
'36' => '徳島県',
|
|
45
|
+
'37' => '香川県',
|
|
46
|
+
'38' => '愛媛県',
|
|
47
|
+
'39' => '高知県',
|
|
48
|
+
'40' => '福岡県',
|
|
49
|
+
'41' => '佐賀県',
|
|
50
|
+
'42' => '長崎県',
|
|
51
|
+
'43' => '熊本県',
|
|
52
|
+
'44' => '大分県',
|
|
53
|
+
'45' => '宮崎県',
|
|
54
|
+
'46' => '鹿児島県',
|
|
55
|
+
'47' => '沖縄県'
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
NAME_TO_CODE = CODE_TO_NAME.invert.freeze
|
|
59
|
+
|
|
60
|
+
# 都道府県コードから都道府県名を返す
|
|
61
|
+
# @rbs (String | Integer | nil) -> String?
|
|
62
|
+
# @param code [String, Integer, nil] 都道府県コード(01–47)。数値またはゼロパディング文字列
|
|
63
|
+
# @return [String, nil] 都道府県名。該当なし・不正入力時は nil
|
|
64
|
+
def self.name_from_code(code)
|
|
65
|
+
return nil if code.nil?
|
|
66
|
+
return nil if code.is_a?(String) && code.strip.empty?
|
|
67
|
+
|
|
68
|
+
key = normalize_code(code)
|
|
69
|
+
return nil unless key
|
|
70
|
+
return nil if key.to_i < 1 || key.to_i > 47
|
|
71
|
+
|
|
72
|
+
CODE_TO_NAME[key]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 都道府県名(正式名称)から都道府県コードを2桁文字列で返す
|
|
76
|
+
# @rbs (String?) -> String?
|
|
77
|
+
# @param name [String, nil] 都道府県の正式名称
|
|
78
|
+
# @return [String, nil] 2桁のコード(例: "13")。該当なし時は nil
|
|
79
|
+
def self.code_from_name(name)
|
|
80
|
+
return nil if name.nil?
|
|
81
|
+
return nil if name.is_a?(String) && name.strip.empty?
|
|
82
|
+
|
|
83
|
+
NAME_TO_CODE[name]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.normalize_code(code)
|
|
87
|
+
case code
|
|
88
|
+
when Integer
|
|
89
|
+
(1..47).cover?(code) ? format('%02d', code) : nil
|
|
90
|
+
when String
|
|
91
|
+
normalize_code_string(code.strip)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
private_class_method :normalize_code
|
|
95
|
+
|
|
96
|
+
def self.normalize_code_string(stripped)
|
|
97
|
+
return nil if stripped.empty?
|
|
98
|
+
return nil unless stripped.match?(/\A\d{1,2}\z/)
|
|
99
|
+
|
|
100
|
+
n = stripped.to_i
|
|
101
|
+
(1..47).cover?(n) ? format('%02d', n) : nil
|
|
102
|
+
end
|
|
103
|
+
private_class_method :normalize_code_string
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'rails'
|
|
5
|
+
|
|
6
|
+
module JpAddressComplement
|
|
7
|
+
# Rails との統合を担う Railtie
|
|
8
|
+
# @rbs inherits Rails::Railtie
|
|
9
|
+
class Railtie < Rails::Railtie
|
|
10
|
+
railtie_name :jp_address_complement
|
|
11
|
+
|
|
12
|
+
rake_tasks do
|
|
13
|
+
load File.expand_path('../tasks/jp_address_complement.rake', __dir__.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
initializer 'jp_address_complement.setup_repository' do # steep:ignore
|
|
17
|
+
require_relative 'repositories/active_record_postal_code_repository'
|
|
18
|
+
require_relative 'models/postal_code'
|
|
19
|
+
JpAddressComplement.configuration.repository ||=
|
|
20
|
+
Repositories::ActiveRecordPostalCodeRepository.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
generators do
|
|
24
|
+
require_relative '../generators/jp_address_complement/install_generator'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative 'postal_code_repository'
|
|
5
|
+
require_relative '../address_record'
|
|
6
|
+
|
|
7
|
+
module JpAddressComplement
|
|
8
|
+
module Repositories
|
|
9
|
+
# ActiveRecord を使用した PostalCodeRepository の実装
|
|
10
|
+
# Rails 環境向けにバンドルされる標準実装
|
|
11
|
+
class ActiveRecordPostalCodeRepository < PostalCodeRepository
|
|
12
|
+
# @rbs (String code) -> Array[AddressRecord]
|
|
13
|
+
# @param code [String] 正規化済み7桁郵便番号
|
|
14
|
+
# @return [Array<AddressRecord>]
|
|
15
|
+
def find_by_code(code)
|
|
16
|
+
postal_code_model.where(postal_code: code).map { |ar| to_record(ar) } # steep:ignore
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs (String prefix) -> Array[AddressRecord]
|
|
20
|
+
# @param prefix [String] 4桁以上の郵便番号プレフィックス
|
|
21
|
+
# @return [Array<AddressRecord>]
|
|
22
|
+
def find_by_prefix(prefix)
|
|
23
|
+
postal_code_model.where('postal_code LIKE ?', "#{prefix}%").map { |ar| to_record(ar) } # steep:ignore
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs (pref: String?, city: String?, ?town: String?) -> Array[AddressRecord]
|
|
27
|
+
def find_postal_codes_by_address(pref:, city:, town: nil)
|
|
28
|
+
relation = address_relation(pref: pref, city: city, town: town)
|
|
29
|
+
return [] unless relation
|
|
30
|
+
|
|
31
|
+
relation.map { |ar| to_record(ar) } # steep:ignore
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# @rbs (pref: String?, city: String?, ?town: String?) -> untyped
|
|
37
|
+
def address_relation(pref:, city:, town: nil)
|
|
38
|
+
return nil if pref.nil? || pref.to_s.strip.empty?
|
|
39
|
+
return nil if city.nil? || city.to_s.strip.empty?
|
|
40
|
+
|
|
41
|
+
relation = postal_code_model.where(pref: pref, city: city)
|
|
42
|
+
return relation if town.blank?
|
|
43
|
+
|
|
44
|
+
pattern = "#{escape_like(town.to_s.strip)}%"
|
|
45
|
+
relation.where('town LIKE ?', pattern)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs () -> singleton(PostalCode)
|
|
49
|
+
def postal_code_model
|
|
50
|
+
JpAddressComplement::PostalCode
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# LIKE 句用に % _ \ をエスケープする
|
|
54
|
+
# @rbs (String) -> String
|
|
55
|
+
def escape_like(str)
|
|
56
|
+
str.gsub(/[%_\\]/) { "\\#{Regexp.last_match(0)}" }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ActiveRecord の動的属性のため Steep の型が不足する。理由明記で抑制(research §7)
|
|
60
|
+
# @rbs (untyped postal_code_ar) -> AddressRecord
|
|
61
|
+
def to_record(postal_code_ar)
|
|
62
|
+
AddressRecord.new(
|
|
63
|
+
postal_code: postal_code_ar.postal_code,
|
|
64
|
+
pref_code: postal_code_ar.pref_code,
|
|
65
|
+
pref: postal_code_ar.pref,
|
|
66
|
+
city: postal_code_ar.city,
|
|
67
|
+
town: postal_code_ar.town,
|
|
68
|
+
kana_pref: postal_code_ar.kana_pref,
|
|
69
|
+
kana_city: postal_code_ar.kana_city,
|
|
70
|
+
kana_town: postal_code_ar.kana_town,
|
|
71
|
+
has_alias: postal_code_ar.has_alias,
|
|
72
|
+
is_partial: postal_code_ar.is_partial,
|
|
73
|
+
is_large_office: postal_code_ar.is_large_office
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require 'csv'
|
|
6
|
+
require_relative 'postal_code_repository'
|
|
7
|
+
require_relative '../address_record'
|
|
8
|
+
|
|
9
|
+
module JpAddressComplement
|
|
10
|
+
module Repositories
|
|
11
|
+
# UTF-8 版 KEN_ALL(utf_ken_all.csv)形式の CSV ファイルを直接読み込んで検索を行う Repository 実装
|
|
12
|
+
#
|
|
13
|
+
# - ActiveRecord や DB に依存せず、純粋に CSV から AddressRecord を構築する
|
|
14
|
+
# - 初回アクセス時に CSV 全体を読み込み、インメモリでインデックスを構築する(2回目以降はメモリ検索のみ)
|
|
15
|
+
# - CSV は日本郵便公式の KEN_ALL.CSV(UTF-8 版 utf_ken_all.csv)と同じ列構成を前提とする
|
|
16
|
+
#
|
|
17
|
+
# 典型的な利用例:
|
|
18
|
+
#
|
|
19
|
+
# require 'jp_address_complement/repositories/csv_postal_code_repository'
|
|
20
|
+
#
|
|
21
|
+
# JpAddressComplement.configure do |c|
|
|
22
|
+
# c.repository = JpAddressComplement::Repositories::CsvPostalCodeRepository.new('/path/to/utf_ken_all.csv')
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class CsvPostalCodeRepository < PostalCodeRepository
|
|
26
|
+
# 列インデックス(KEN_ALL.CSV 形式)
|
|
27
|
+
COL_PREF_CODE = 0 # : Integer
|
|
28
|
+
COL_POSTAL_CODE = 2 # : Integer
|
|
29
|
+
COL_KANA_PREF = 3 # : Integer
|
|
30
|
+
COL_KANA_CITY = 4 # : Integer
|
|
31
|
+
COL_KANA_TOWN = 5 # : Integer
|
|
32
|
+
COL_PREF = 6 # : Integer
|
|
33
|
+
COL_CITY = 7 # : Integer
|
|
34
|
+
COL_TOWN = 8 # : Integer
|
|
35
|
+
COL_IS_PARTIAL = 9 # : Integer
|
|
36
|
+
COL_HAS_ALIAS = 12 # : Integer
|
|
37
|
+
COL_IS_LARGE_OFFICE = 13 # : Integer
|
|
38
|
+
|
|
39
|
+
# @rbs (String csv_path) -> void
|
|
40
|
+
# @param csv_path [String] 読み込む KEN_ALL 形式 UTF-8 CSV のパス
|
|
41
|
+
def initialize(csv_path)
|
|
42
|
+
super()
|
|
43
|
+
@csv_path = csv_path
|
|
44
|
+
@loaded = false
|
|
45
|
+
@records = [] # : Array[AddressRecord]
|
|
46
|
+
@by_code = Hash.new { |h, k| h[k] = [] } # : Hash[String, Array[AddressRecord]]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @rbs (String code) -> Array[AddressRecord]
|
|
50
|
+
def find_by_code(code)
|
|
51
|
+
ensure_loaded
|
|
52
|
+
@by_code[code] || []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @rbs (String prefix) -> Array[AddressRecord]
|
|
56
|
+
def find_by_prefix(prefix)
|
|
57
|
+
ensure_loaded
|
|
58
|
+
@records.select { |r| r.postal_code.start_with?(prefix) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @rbs (pref: String?, city: String?, ?town: String?) -> Array[AddressRecord]
|
|
62
|
+
def find_postal_codes_by_address(pref:, city:, town: nil)
|
|
63
|
+
ensure_loaded
|
|
64
|
+
return [] if blank?(pref) || blank?(city)
|
|
65
|
+
|
|
66
|
+
town_query = town&.to_s&.strip
|
|
67
|
+
pref_s = pref.to_s
|
|
68
|
+
city_s = city.to_s
|
|
69
|
+
@records.select { |record| address_match?(record, pref_s, city_s, town_query) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# @rbs () -> void
|
|
75
|
+
def ensure_loaded
|
|
76
|
+
return if @loaded
|
|
77
|
+
|
|
78
|
+
load_csv
|
|
79
|
+
@loaded = true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @rbs () -> void
|
|
83
|
+
def load_csv
|
|
84
|
+
validate_csv_path!
|
|
85
|
+
@records.clear
|
|
86
|
+
@by_code.clear
|
|
87
|
+
each_csv_row { |row| append_record(row) }
|
|
88
|
+
rescue Errno::ENOENT
|
|
89
|
+
raise JpAddressComplement::Error, "CSV ファイルが見つかりません: #{@csv_path}"
|
|
90
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError, CSV::InvalidEncodingError => e
|
|
91
|
+
raise JpAddressComplement::Error, "CSV のエンコーディング変換に失敗しました: #{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @rbs () -> void
|
|
95
|
+
def validate_csv_path!
|
|
96
|
+
raise JpAddressComplement::Error, 'CSV ファイルが指定されていません' if @csv_path.nil? || @csv_path.to_s.empty?
|
|
97
|
+
raise JpAddressComplement::Error, "CSV ファイルが見つかりません: #{@csv_path}" unless File.exist?(@csv_path)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @rbs () { (Array[String?]) -> void } -> void
|
|
101
|
+
def each_csv_row(&)
|
|
102
|
+
CSV.foreach(@csv_path, encoding: 'UTF-8', &)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @rbs (Array[String?] row) -> void
|
|
106
|
+
def append_record(row)
|
|
107
|
+
record = build_record_from_row(row)
|
|
108
|
+
return if record.nil?
|
|
109
|
+
|
|
110
|
+
@records << record
|
|
111
|
+
@by_code[record.postal_code] << record
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @rbs (AddressRecord record, String pref, String city, String? town_query) -> bool
|
|
115
|
+
def address_match?(record, pref, city, town_query)
|
|
116
|
+
return false unless record.pref == pref && record.city == city
|
|
117
|
+
return true if town_query.nil? || town_query.empty?
|
|
118
|
+
|
|
119
|
+
record.town.to_s.start_with?(town_query)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @rbs (Array[String?] row) -> (AddressRecord | nil)
|
|
123
|
+
def build_record_from_row(row)
|
|
124
|
+
attrs = parse_row_attrs(row)
|
|
125
|
+
return nil unless attrs
|
|
126
|
+
|
|
127
|
+
AddressRecord.new(
|
|
128
|
+
postal_code: attrs[:postal_code],
|
|
129
|
+
pref_code: attrs[:pref_code],
|
|
130
|
+
pref: attrs[:pref],
|
|
131
|
+
city: attrs[:city],
|
|
132
|
+
town: attrs[:town],
|
|
133
|
+
kana_pref: attrs[:kana_pref],
|
|
134
|
+
kana_city: attrs[:kana_city],
|
|
135
|
+
kana_town: attrs[:kana_town],
|
|
136
|
+
has_alias: attrs[:has_alias],
|
|
137
|
+
is_partial: attrs[:is_partial],
|
|
138
|
+
is_large_office: attrs[:is_large_office]
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @rbs (Array[String?] row) -> (Hash[Symbol, untyped] | nil)
|
|
143
|
+
def parse_row_attrs(row)
|
|
144
|
+
required = extract_required_fields(row)
|
|
145
|
+
return nil unless required && valid_postal_code_format?(required[0])
|
|
146
|
+
|
|
147
|
+
build_row_attrs(row, required[0], required[1], required[2], required[3])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @rbs (Array[String?] row) -> (Array[String] | nil)
|
|
151
|
+
def extract_required_fields(row)
|
|
152
|
+
postal_code = strip_cell(row[COL_POSTAL_CODE])
|
|
153
|
+
pref_code = strip_cell(row[COL_PREF_CODE])&.slice(0, 2)
|
|
154
|
+
pref = strip_cell(row[COL_PREF])
|
|
155
|
+
city = strip_cell(row[COL_CITY])
|
|
156
|
+
return nil if [postal_code, pref_code, pref, city].any?(&:nil?)
|
|
157
|
+
|
|
158
|
+
# 上記ガードで nil は除外済み。型を Array[String] に合わせるため to_s で明示
|
|
159
|
+
[postal_code.to_s, pref_code.to_s, pref.to_s, city.to_s]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @rbs (String? cell) -> (String | nil)
|
|
163
|
+
def strip_cell(cell)
|
|
164
|
+
cell&.strip
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @rbs (String? postal_code) -> bool
|
|
168
|
+
def valid_postal_code_format?(postal_code)
|
|
169
|
+
!postal_code.nil? && postal_code.match?(/\A\d{7}\z/)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @rbs (Array[String?], String, String, String, String) -> Hash[Symbol, untyped]
|
|
173
|
+
def build_row_attrs(row, postal_code, pref_code, pref, city)
|
|
174
|
+
{
|
|
175
|
+
postal_code: postal_code,
|
|
176
|
+
pref_code: pref_code,
|
|
177
|
+
pref: pref,
|
|
178
|
+
city: city,
|
|
179
|
+
town: row[COL_TOWN]&.strip,
|
|
180
|
+
kana_pref: row[COL_KANA_PREF]&.strip,
|
|
181
|
+
kana_city: row[COL_KANA_CITY]&.strip,
|
|
182
|
+
kana_town: row[COL_KANA_TOWN]&.strip,
|
|
183
|
+
has_alias: flag?(row[COL_HAS_ALIAS]),
|
|
184
|
+
is_partial: flag?(row[COL_IS_PARTIAL]),
|
|
185
|
+
is_large_office: flag?(row[COL_IS_LARGE_OFFICE])
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @rbs (String? cell) -> bool
|
|
190
|
+
def flag?(cell)
|
|
191
|
+
cell&.strip == '1'
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @rbs (untyped value) -> bool
|
|
195
|
+
def blank?(value)
|
|
196
|
+
value.nil? || value.to_s.strip.empty?
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module JpAddressComplement
|
|
5
|
+
module Repositories
|
|
6
|
+
# データアクセスを抽象化するインターフェース基底クラス
|
|
7
|
+
# 具体的な実装は ActiveRecordPostalCodeRepository またはユーザー定義アダプターで行う
|
|
8
|
+
class PostalCodeRepository
|
|
9
|
+
# 7桁郵便番号で完全一致検索する
|
|
10
|
+
# @rbs (String code) -> Array[AddressRecord]
|
|
11
|
+
# @param code [String] 正規化済み7桁郵便番号
|
|
12
|
+
# @return [Array<AddressRecord>]
|
|
13
|
+
def find_by_code(code)
|
|
14
|
+
raise NotImplementedError, "#{self.class}#find_by_code を実装してください"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 郵便番号プレフィックスで前方一致検索する
|
|
18
|
+
# @rbs (String prefix) -> Array[AddressRecord]
|
|
19
|
+
# @param prefix [String] 4桁以上の郵便番号プレフィックス
|
|
20
|
+
# @return [Array<AddressRecord>]
|
|
21
|
+
def find_by_prefix(prefix)
|
|
22
|
+
raise NotImplementedError, "#{self.class}#find_by_prefix を実装してください"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 都道府県・市区町村・町域で検索する(逆引き)。町域は前方一致。
|
|
26
|
+
# @rbs (pref: String?, city: String?, ?town: String?) -> Array[AddressRecord]
|
|
27
|
+
# @param pref [String] 都道府県名(正式名称)
|
|
28
|
+
# @param city [String] 市区町村名
|
|
29
|
+
# @param town [String, nil] 町域名。省略時は都道府県+市区町村のみ。指定時は前方一致で候補を返す
|
|
30
|
+
# @return [Array<AddressRecord>] 該当レコードの配列。該当なし・入力不十分時は []
|
|
31
|
+
def find_postal_codes_by_address(pref:, city:, town: nil)
|
|
32
|
+
raise NotImplementedError, "#{self.class}#find_postal_codes_by_address を実装してください"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|