jp_address 1.0.1 → 2.0.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 +4 -4
- data/CHANGELOG.md +57 -0
- data/MIT-LICENSE +0 -0
- data/README.md +98 -97
- data/Rakefile +14 -15
- data/app/controllers/jp_address/application_controller.rb +1 -0
- data/app/controllers/jp_address/zipcodes_controller.rb +9 -4
- data/app/models/jp_address/application_record.rb +5 -0
- data/app/models/jp_address/zipcode.rb +166 -36
- data/config/routes.rb +0 -0
- data/db/migrate/20160312045953_create_jp_address_zipcodes.rb +1 -1
- data/lib/jp_address/engine.rb +0 -0
- data/lib/jp_address/version.rb +1 -1
- data/lib/jp_address.rb +3 -1
- data/lib/tasks/jp_address_tasks.rake +0 -0
- metadata +48 -157
- data/app/assets/javascripts/jp_address/application.js +0 -13
- data/app/assets/stylesheets/jp_address/application.css +0 -15
- data/app/helpers/jp_address/application_helper.rb +0 -4
- data/app/views/layouts/jp_address/application.html.erb +0 -14
- data/spec/controllers/jp_address/zipcodes_controller_spec.rb +0 -25
- data/spec/dummy/README.rdoc +0 -28
- data/spec/dummy/Rakefile +0 -6
- data/spec/dummy/app/assets/config/manifest.js +0 -2
- data/spec/dummy/app/assets/javascripts/application.js +0 -13
- data/spec/dummy/app/assets/stylesheets/application.css +0 -15
- data/spec/dummy/app/controllers/application_controller.rb +0 -5
- data/spec/dummy/app/helpers/application_helper.rb +0 -2
- data/spec/dummy/app/views/layouts/application.html.erb +0 -14
- data/spec/dummy/bin/bundle +0 -3
- data/spec/dummy/bin/rails +0 -4
- data/spec/dummy/bin/rake +0 -4
- data/spec/dummy/bin/setup +0 -29
- data/spec/dummy/config/application.rb +0 -31
- data/spec/dummy/config/boot.rb +0 -5
- data/spec/dummy/config/database.yml +0 -25
- data/spec/dummy/config/environment.rb +0 -5
- data/spec/dummy/config/environments/development.rb +0 -41
- data/spec/dummy/config/environments/production.rb +0 -79
- data/spec/dummy/config/environments/test.rb +0 -42
- data/spec/dummy/config/initializers/assets.rb +0 -11
- data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/dummy/config/initializers/inflections.rb +0 -16
- data/spec/dummy/config/initializers/mime_types.rb +0 -4
- data/spec/dummy/config/initializers/session_store.rb +0 -3
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/spec/dummy/config/locales/en.yml +0 -23
- data/spec/dummy/config/routes.rb +0 -3
- data/spec/dummy/config/secrets.yml +0 -22
- data/spec/dummy/config.ru +0 -4
- data/spec/dummy/db/schema.rb +0 -25
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +0 -6
- data/spec/dummy/public/404.html +0 -67
- data/spec/dummy/public/422.html +0 -67
- data/spec/dummy/public/500.html +0 -66
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/jp_address_zipcodes.rb +0 -10
- data/spec/models/jp_address/zipcode_spec.rb +0 -29
- data/spec/rails_helper.rb +0 -15
- data/spec/spec_helper.rb +0 -92
- data/spec/support/files/sample_ken.csv +0 -5
- data/spec/vcr/download_master_file_from_japanpost.yml +0 -37476
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67701127b1fbec6ae9292647b6373acdeca9dcb51b78addeefaeed7544852531
|
|
4
|
+
data.tar.gz: d3fb7f7ee44c1976825cb22c0394414a9dec14aa220536e17bf57daa382a7575
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33814d00bd9491bce91816e44066be23025af90b8c68ff62a3924fe6ed1316d5f08339864642a3848f55a7cff23838712816ec5747edabe633e8ace33c91a8fa
|
|
7
|
+
data.tar.gz: becaad6991cc0cc30d8dc05f479488500b02707c3028c6b8841af2ed3e35a2042e01c5332a15ce20caa9962deb4231b629f9f83503f4e09a0bd0b9b7e1a36c74
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
最新の Ruby / Rails への対応と、郵便番号データの取り込みまわりのバグ修正。
|
|
6
|
+
|
|
7
|
+
### 対応バージョン
|
|
8
|
+
|
|
9
|
+
* Ruby 3.1 〜 3.4、Rails 7.1 / 8.0 / 8.1 に対応。
|
|
10
|
+
(Ruby 3.1 + Rails 7.1 〜 Ruby 3.4 + Rails 8.1 で実際にテスト済み)
|
|
11
|
+
* Rails 6.x 以前のサポートを終了。
|
|
12
|
+
* Ruby 3.4 で `csv` が bundled gem になったため、gemspec の依存に追加。
|
|
13
|
+
これがないと `require 'csv'` に失敗する。
|
|
14
|
+
* `railties` / `activerecord` を runtime 依存として明示。
|
|
15
|
+
* rubyzip 3.x に対応(`>= 2.3, < 4.0`)。
|
|
16
|
+
|
|
17
|
+
### バグ修正
|
|
18
|
+
|
|
19
|
+
* **町域名が複数行に分かれている郵便番号で町名が消えていた**。
|
|
20
|
+
ken_all.csv は町域名が長いと括弧の途中で改行して次の行に続くが、その
|
|
21
|
+
継続行を別の町名として扱っていたため、共通部分が見つからず町名が空に
|
|
22
|
+
なっていた。括弧が閉じるまでの行を読み飛ばすようにした。
|
|
23
|
+
例:9292225(石川県羽咋郡宝達志水町 米出)が `""` → `"米出"`。
|
|
24
|
+
* **同じ郵便番号の行が全部同じ町名でも町名が空になっていた**。
|
|
25
|
+
`_find_shared_name_from` が「差分が見つからない=共通部分なし」と
|
|
26
|
+
判定していたのを修正。短い方の地名がまるごと共通部分になる場合
|
|
27
|
+
(例:東中島/東中島本町)もその地名を返すようにした。
|
|
28
|
+
* 日本郵便へのリクエストがリダイレクト・エラーレスポンスを一切見ていな
|
|
29
|
+
かったのを修正。リダイレクトは最大 5 回まで追い、200 以外は
|
|
30
|
+
`JpAddress::Zipcode::DownloadError` を投げる。
|
|
31
|
+
* zip の中から CSV エントリだけを取り出すようにした(従来は全エントリを
|
|
32
|
+
同じパスに展開しようとしていた)。
|
|
33
|
+
* 存在しない CSV パスを `load_master_data` に渡すと、無駄にダウンロード
|
|
34
|
+
してから `Errno::ENOENT` になっていたのを `ArgumentError` に。
|
|
35
|
+
* `row[8]` が空の行で `NoMethodError` にならないようにした。
|
|
36
|
+
* `FileUtils` を require していなかったのを修正。
|
|
37
|
+
|
|
38
|
+
### 変更
|
|
39
|
+
|
|
40
|
+
* `load_master_data` を全面的に書き直し。行ごとの生 SQL を `insert_all!`
|
|
41
|
+
でのバッチ投入に変え、統合処理も CSV 読み込み中にメモリ上で行うように
|
|
42
|
+
したので、大幅に速くなった(本番でのロードが数分 → 数十秒程度)。
|
|
43
|
+
これに伴い内部メソッド `_merge_same_zip_addresses` は削除。
|
|
44
|
+
* API のレスポンスを `render plain:` から `render json:` に変更。
|
|
45
|
+
Content-Type が `text/plain` から `application/json` になる。
|
|
46
|
+
* 郵便番号の全角数字(`533-0033`)を受け付けるようにした。
|
|
47
|
+
* `JpAddress::ApplicationRecord` を追加し、`Zipcode` の親クラスにした。
|
|
48
|
+
* Sprockets 前提の app/assets・レイアウト・空の ApplicationHelper を削除。
|
|
49
|
+
JSON しか返さない engine には不要で、propshaft / importmap 環境では
|
|
50
|
+
むしろ壊れるため。
|
|
51
|
+
* テストを VCR カセット(2.5MB のバイナリ)から WebMock のスタブに置き換え。
|
|
52
|
+
コントローラスペックはリクエストスペックに変更。
|
|
53
|
+
* GitHub Actions で Ruby × Rails のマトリクステストを追加。
|
|
54
|
+
|
|
55
|
+
## 1.0.2 以前
|
|
56
|
+
|
|
57
|
+
省略。
|
data/MIT-LICENSE
CHANGED
|
File without changes
|
data/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Ruby on Rails
|
|
1
|
+
# Ruby on Rails で 郵便番号住所検索 な gem
|
|
2
2
|
|
|
3
3
|
## JpAddressとは
|
|
4
|
-
日本郵便の「[郵便番号データ](https://www.post.japanpost.jp/zipcode/
|
|
4
|
+
日本郵便の「[郵便番号データ](https://www.post.japanpost.jp/zipcode/download.html)」を用いて、あなたの Rails サイトに「郵便番号からの住所検索機能」を組み込むための gem です。
|
|
5
5
|
以下の機能を提供します。
|
|
6
6
|
|
|
7
|
-
* [[郵便番号データ](https://www.post.japanpost.jp/zipcode/dl/
|
|
7
|
+
* [[郵便番号データ](https://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip "ken_all.zip")]をダウンロードして自前DBのテーブル(jp_address_zipcodes)にロードするクラスメソッド。(```JpAddress::Zipcode.load_master_data```)
|
|
8
8
|
* 郵便番号を受け取り都道府県名と住所をJSONで返却するAPI。
|
|
9
9
|
(```jp_address/zipcodes#search```)
|
|
10
10
|
|
|
@@ -14,8 +14,17 @@ APIはお使いのRailsアプリケーションにマウントして使います
|
|
|
14
14
|
あと必要なのは、戻ってくるJSONを加工してHTML要素にセットするJavaScriptの記述だけです。<br>
|
|
15
15
|
(本記事下部にサンプルコードを掲載しています。)
|
|
16
16
|
|
|
17
|
+
### 対応バージョン
|
|
18
|
+
|
|
19
|
+
| | 対応 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Ruby | 3.1 以降 |
|
|
22
|
+
| Rails | 7.1 / 8.0 / 8.1 |
|
|
23
|
+
|
|
24
|
+
Rails 6.x 以前をお使いの場合は 1.0.2 をご利用ください。
|
|
25
|
+
|
|
17
26
|
### インストール
|
|
18
|
-
|
|
27
|
+
Gemfileに追記
|
|
19
28
|
```ruby
|
|
20
29
|
gem 'jp_address'
|
|
21
30
|
```
|
|
@@ -36,13 +45,34 @@ $ bundle exec rails runner -e development 'JpAddress::Zipcode.load_master_data'
|
|
|
36
45
|
$ bundle exec rails runner -e production 'JpAddress::Zipcode.load_master_data'
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
APP_ROOT/tmp/ を作業ディレクトリに使用しています。<br>
|
|
49
|
+
最初にテーブルをトランケートしますので、毎回「全件insert」になります。<br>
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
すでに手元に ken_all.csv がある場合は、パスを渡せばダウンロードを省略できます。<br>
|
|
52
|
+
(文字コードは日本郵便の配布どおり CP932 のままで構いません。)
|
|
53
|
+
```ruby
|
|
54
|
+
JpAddress::Zipcode.load_master_data('tmp/ken_all.csv')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
同じ郵便番号を持つレコードは統合されます。<br>
|
|
58
|
+
<br>
|
|
59
|
+
例:9896712<br>
|
|
60
|
+
```
|
|
61
|
+
"宮城県","大崎市","鳴子温泉水沼"
|
|
62
|
+
"宮城県","大崎市","鳴子温泉南山"
|
|
63
|
+
"宮城県","大崎市","鳴子温泉山際"
|
|
64
|
+
"宮城県","大崎市","鳴子温泉和田"
|
|
65
|
+
```
|
|
66
|
+
これらは先頭から共通する地名を探し、うまく見つかれば
|
|
67
|
+
```
|
|
68
|
+
"宮城県","大崎市","鳴子温泉"
|
|
69
|
+
```
|
|
70
|
+
として1つのレコードにします。
|
|
71
|
+
共通する地名が抜き出せない場合は空の町名にします。
|
|
43
72
|
|
|
44
73
|
### APIのマウント
|
|
45
74
|
Railsアプリの config/routes.rb に追記。
|
|
75
|
+
at: に渡す値は /jp_address でなくても /service や /api などでも構いません。
|
|
46
76
|
```ruby
|
|
47
77
|
mount JpAddress::Engine, at: "/jp_address"
|
|
48
78
|
```
|
|
@@ -61,6 +91,14 @@ http://localhost:3000/jp_address/zipcodes/search?zip=5330033
|
|
|
61
91
|
{"id":84280,"zip":"5330033","prefecture":"大阪府","city":"大阪市東淀川区","town":"東中島"}
|
|
62
92
|
```
|
|
63
93
|
|
|
94
|
+
該当する郵便番号がない場合は、各項目が null の JSON が返ります。
|
|
95
|
+
```js script
|
|
96
|
+
{"id":null,"zip":null,"prefecture":null,"city":null,"town":null}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
半角ハイフン・空白・全角数字は自動的に取り除かれるので、`533-0033` や `533-0033` を
|
|
100
|
+
そのまま投げても構いません。
|
|
101
|
+
|
|
64
102
|
### APIを利用するサンプル JavaScript
|
|
65
103
|
フォームに
|
|
66
104
|
1. #zipcode (郵便番号を入力するテキストボックス)
|
|
@@ -68,15 +106,11 @@ http://localhost:3000/jp_address/zipcodes/search?zip=5330033
|
|
|
68
106
|
3. #address (住所を表示するテキストボックス)
|
|
69
107
|
|
|
70
108
|
の3要素があるとします。<br>
|
|
71
|
-
#zipcodeに入れられた値を
|
|
109
|
+
#zipcodeに入れられた値を input イベントで拾ってAPIを叩き、都道府県プルダウンを選択し、住所をセットするサンプルです。<br>
|
|
72
110
|
|
|
73
111
|
都道府県プルダウンは、戻ってくるJSONの "prefecture" すなわち都道府県名で選択します。<br>
|
|
74
112
|
ですので、お持ちの都道府県マスターの各レコードがどのようなIDを持っていても構いません。
|
|
75
113
|
|
|
76
|
-
※ JQuery の存在を前提にしています。<br>
|
|
77
|
-
※ 郵便番号の半角ハイフンは自動でカットされます。<br>
|
|
78
|
-
※ もともと CoffeeScript で書いてあったソースを decaffeinate したものですので冗長です。本質的な処理はAddressSearch が担っているだけで、他の関数は decaffeinate に必要なだけです。
|
|
79
|
-
|
|
80
114
|
#### フォーム
|
|
81
115
|
```
|
|
82
116
|
<form>
|
|
@@ -134,106 +168,73 @@ http://localhost:3000/jp_address/zipcodes/search?zip=5330033
|
|
|
134
168
|
</form>
|
|
135
169
|
```
|
|
136
170
|
|
|
137
|
-
####
|
|
171
|
+
#### JavaScript
|
|
172
|
+
jQuery などのライブラリは不要です。app/javascript 配下など、フォームのあるページで
|
|
173
|
+
読み込まれる場所に置いてください。
|
|
138
174
|
|
|
139
175
|
```js script
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
class AddressSearch {
|
|
177
|
+
constructor(zipSelector, prefectureSelector, addressSelector, endpoint = '/jp_address/zipcodes/search') {
|
|
178
|
+
this.zip = document.querySelector(zipSelector);
|
|
179
|
+
this.prefecture = document.querySelector(prefectureSelector);
|
|
180
|
+
this.address = document.querySelector(addressSelector);
|
|
181
|
+
this.endpoint = endpoint;
|
|
144
182
|
}
|
|
145
183
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
descriptor.enumerable = descriptor.enumerable || false;
|
|
150
|
-
descriptor.configurable = true;
|
|
184
|
+
start() {
|
|
185
|
+
this.zip.addEventListener('input', () => this.execute());
|
|
186
|
+
}
|
|
151
187
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
async execute() {
|
|
189
|
+
const zip = this.zip.value.replace(/[^0-90-9]/g, '');
|
|
190
|
+
if (zip.length !== 7) return;
|
|
191
|
+
|
|
192
|
+
const url = `${this.endpoint}?zip=${encodeURIComponent(zip)}`;
|
|
193
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
194
|
+
if (!res.ok) return;
|
|
195
|
+
|
|
196
|
+
const json = await res.json();
|
|
197
|
+
if (json.id === null) {
|
|
198
|
+
this.clear();
|
|
199
|
+
} else {
|
|
200
|
+
this.setPrefecture(json.prefecture);
|
|
201
|
+
this.address.value = `${json.city}${json.town}`;
|
|
155
202
|
}
|
|
156
203
|
}
|
|
157
204
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (staticProps)
|
|
162
|
-
_defineProperties(Constructor, staticProps);
|
|
163
|
-
return Constructor;
|
|
205
|
+
clear() {
|
|
206
|
+
this.prefecture.selectedIndex = 0;
|
|
207
|
+
this.address.value = '';
|
|
164
208
|
}
|
|
165
209
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.address = $(address_elem_id);
|
|
173
|
-
this.prefecture_elem_id = prefecture_elem_id;
|
|
210
|
+
setPrefecture(name) {
|
|
211
|
+
for (const option of this.prefecture.options) {
|
|
212
|
+
if (option.text === name) {
|
|
213
|
+
option.selected = true;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
174
216
|
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
175
219
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
value: function _clear_current_value() {
|
|
184
|
-
$(this.prefecture_elem_id + ' >option:eq(0)').prop('selected', true);
|
|
185
|
-
return this.address.val('');
|
|
186
|
-
}
|
|
187
|
-
}, {
|
|
188
|
-
key: "_set_prefecture",
|
|
189
|
-
value: function _set_prefecture(json) {
|
|
190
|
-
return $(this.prefecture_elem_id + ' > option').each(function() {
|
|
191
|
-
if ($(this).text() === json['prefecture']) {
|
|
192
|
-
return $(this).prop('selected', true);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}, {
|
|
197
|
-
key: "_set_address",
|
|
198
|
-
value: function _set_address(json) {
|
|
199
|
-
return this.address.val(json['city'] + json['town']);
|
|
200
|
-
}
|
|
201
|
-
}, {
|
|
202
|
-
key: "_call_api",
|
|
203
|
-
value: function _call_api() {
|
|
204
|
-
var _this = this;
|
|
205
|
-
return $.getJSON('/jp_address/zipcodes/search', {zip: this.zip.val()}, function(json) {
|
|
206
|
-
if (json['id'] === null) {
|
|
207
|
-
return _this._clear_current_value();
|
|
208
|
-
} else {
|
|
209
|
-
_this._set_prefecture(json);
|
|
210
|
-
return _this._set_address(json);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}, {
|
|
215
|
-
key: "execute",
|
|
216
|
-
value: function execute() {
|
|
217
|
-
this._remove_hyphen();
|
|
218
|
-
if (this.zip.val().length === 7) {
|
|
219
|
-
return this._call_api();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}]);
|
|
220
|
+
// #zipcode, #prefecture_id, #address を各自の環境に合わせて書き換えてください。
|
|
221
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
222
|
+
new AddressSearch('#zipcode', '#prefecture_id', '#address').start();
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 開発
|
|
223
227
|
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
```
|
|
229
|
+
$ bundle install
|
|
230
|
+
$ bundle exec rspec
|
|
226
231
|
```
|
|
227
232
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
$('#zipcode').keyup(function() {
|
|
234
|
-
address_search.execute();
|
|
235
|
-
});
|
|
236
|
-
});
|
|
233
|
+
複数バージョンの Rails で試す場合は gemfiles/ 配下の Gemfile を使ってください。
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
$ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle install
|
|
237
|
+
$ BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rspec
|
|
237
238
|
```
|
|
238
239
|
|
|
239
240
|
##### 作者
|
data/Rakefile
CHANGED
|
@@ -4,23 +4,22 @@ rescue LoadError
|
|
|
4
4
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
rdoc
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
begin
|
|
8
|
+
require 'rdoc/task'
|
|
9
|
+
|
|
10
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
11
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
12
|
+
rdoc.title = 'JpAddress'
|
|
13
|
+
rdoc.options << '--line-numbers'
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
rescue LoadError
|
|
17
|
+
# rdoc is optional; skip the task when it is not installed.
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
APP_RAKEFILE = File.expand_path(
|
|
20
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
|
17
21
|
load 'rails/tasks/engine.rake'
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
load 'rails/tasks/statistics.rake'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
23
|
Bundler::GemHelper.install_tasks
|
|
25
24
|
|
|
26
25
|
require 'rspec/core'
|
|
@@ -29,5 +28,5 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
|
29
28
|
spec.pattern = FileList['spec/**/*_spec.rb']
|
|
30
29
|
end
|
|
31
30
|
|
|
32
|
-
task :
|
|
33
|
-
task :
|
|
31
|
+
task default: :spec
|
|
32
|
+
task test: :spec
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
require_dependency "jp_address/application_controller"
|
|
2
|
-
|
|
3
1
|
module JpAddress
|
|
4
2
|
class ZipcodesController < ApplicationController
|
|
5
3
|
def search
|
|
6
|
-
@zipcode = Zipcode.find_by(
|
|
7
|
-
render
|
|
4
|
+
@zipcode = Zipcode.find_by(zip: _normalized_zip) || Zipcode.new
|
|
5
|
+
render json: @zipcode
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# 全角数字・ハイフン・空白などを取り除いて半角数字だけにします。
|
|
11
|
+
def _normalized_zip
|
|
12
|
+
params[:zip].to_s.tr('0-9', '0-9').gsub(/[^0-9]/, '')
|
|
8
13
|
end
|
|
9
14
|
end
|
|
10
15
|
end
|
|
@@ -1,82 +1,212 @@
|
|
|
1
1
|
module JpAddress
|
|
2
|
-
class Zipcode <
|
|
2
|
+
class Zipcode < ApplicationRecord
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# 日本郵便からダウンロードした CSV の一時保存先。
|
|
5
|
+
MASTER_CSV_PATH = 'tmp/ken_all.csv'.freeze
|
|
6
|
+
|
|
7
|
+
# 郵便番号データ(住所の郵便番号/小書き)の配布 URL。
|
|
8
|
+
MASTER_FILE_URL = 'https://www.post.japanpost.jp/zipcode/dl/kogaki/zip/ken_all.zip'.freeze
|
|
9
|
+
|
|
10
|
+
# insert_all で一度に流し込む件数。
|
|
11
|
+
INSERT_BATCH_SIZE = 1_000
|
|
12
|
+
|
|
13
|
+
# リダイレクトを追う上限。
|
|
14
|
+
MAX_REDIRECTS = 5
|
|
15
|
+
|
|
16
|
+
class DownloadError < StandardError; end
|
|
5
17
|
|
|
6
18
|
def self.download_master_file_from_japanpost
|
|
7
19
|
_setup_directory
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
tmp_zip = _save_zip(_request_to_japanpost)
|
|
21
|
+
begin
|
|
22
|
+
_save_csv(tmp_zip.path)
|
|
23
|
+
ensure
|
|
24
|
+
tmp_zip.close!
|
|
25
|
+
end
|
|
10
26
|
:success if File.exist?(MASTER_CSV_PATH)
|
|
11
27
|
end
|
|
12
28
|
|
|
13
29
|
def self._setup_directory
|
|
14
|
-
FileUtils.mkdir_p
|
|
30
|
+
FileUtils.mkdir_p File.dirname(MASTER_CSV_PATH)
|
|
15
31
|
_remove_csv
|
|
16
32
|
end
|
|
17
33
|
private_class_method :_setup_directory
|
|
18
34
|
|
|
19
|
-
def self._request_to_japanpost
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
def self._request_to_japanpost(url = MASTER_FILE_URL, redirect_limit = MAX_REDIRECTS)
|
|
36
|
+
raise DownloadError, "too many redirects while downloading #{MASTER_FILE_URL}" if redirect_limit <= 0
|
|
37
|
+
|
|
38
|
+
uri = URI.parse(url)
|
|
39
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |https|
|
|
40
|
+
https.get(uri.request_uri)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
case res
|
|
44
|
+
when Net::HTTPSuccess
|
|
45
|
+
res.body
|
|
46
|
+
when Net::HTTPRedirection
|
|
47
|
+
location = res['location']
|
|
48
|
+
raise DownloadError, "got a redirect without a location from #{url}" if location.nil?
|
|
49
|
+
|
|
50
|
+
_request_to_japanpost(URI.join(url, location).to_s, redirect_limit - 1)
|
|
51
|
+
else
|
|
52
|
+
raise DownloadError, "failed to download #{url} (#{res.code} #{res.message})"
|
|
53
|
+
end
|
|
27
54
|
end
|
|
28
55
|
private_class_method :_request_to_japanpost
|
|
29
56
|
|
|
30
57
|
def self._save_zip(binary)
|
|
31
|
-
tmp_zip = Tempfile.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
tmp_zip = Tempfile.new(['ken_all', '.zip'])
|
|
59
|
+
tmp_zip.binmode
|
|
60
|
+
tmp_zip.write binary
|
|
61
|
+
tmp_zip.flush
|
|
62
|
+
tmp_zip.close
|
|
36
63
|
tmp_zip
|
|
37
64
|
end
|
|
38
65
|
private_class_method :_save_zip
|
|
39
66
|
|
|
40
67
|
def self._save_csv(zip_path)
|
|
41
68
|
Zip::File.open(zip_path) do |zip_file|
|
|
42
|
-
zip_file.
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
entry = zip_file.find { |e| e.file? && File.extname(e.name).downcase == '.csv' }
|
|
70
|
+
raise DownloadError, "no CSV file found in the archive from #{MASTER_FILE_URL}" if entry.nil?
|
|
71
|
+
|
|
72
|
+
entry.extract(MASTER_CSV_PATH)
|
|
45
73
|
end
|
|
46
74
|
end
|
|
47
75
|
private_class_method :_save_csv
|
|
48
76
|
|
|
77
|
+
# CSV を読み込んでテーブルを作り直します。
|
|
78
|
+
#
|
|
79
|
+
# 同じ郵便番号を持つ行は1レコードに統合してから insert するので、
|
|
80
|
+
# 郵便番号はテーブル内で一意になります。
|
|
49
81
|
def self.load_master_data(csv_path = MASTER_CSV_PATH)
|
|
50
|
-
|
|
82
|
+
unless File.exist?(csv_path)
|
|
83
|
+
raise ArgumentError, "#{csv_path} does not exist" unless csv_path == MASTER_CSV_PATH
|
|
84
|
+
|
|
85
|
+
download_master_file_from_japanpost
|
|
86
|
+
end
|
|
87
|
+
|
|
51
88
|
_clear_table
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
sanitize_sql(
|
|
55
|
-
["insert into jp_address_zipcodes (zip, prefecture, city, town)
|
|
56
|
-
values ('%s', '%s', '%s', '%s')", row[2], row[6], row[7], _remove_needless_words(row[8])
|
|
57
|
-
]
|
|
58
|
-
)
|
|
59
|
-
)
|
|
89
|
+
transaction do
|
|
90
|
+
_each_record_batch_from(csv_path) { |batch| insert_all!(batch) }
|
|
60
91
|
end
|
|
61
92
|
_remove_csv
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# CSV を郵便番号ごとにまとめ、insert_all に渡せる Hash の配列を
|
|
97
|
+
# INSERT_BATCH_SIZE 件ずつ yield します。
|
|
98
|
+
def self._each_record_batch_from(csv_path)
|
|
99
|
+
_read_master_csv(csv_path).each_value.each_slice(INSERT_BATCH_SIZE) do |slice|
|
|
100
|
+
yield slice.map { |rec|
|
|
101
|
+
{
|
|
102
|
+
zip: rec[:zip],
|
|
103
|
+
prefecture: rec[:prefecture],
|
|
104
|
+
city: rec[:city],
|
|
105
|
+
town: _find_shared_name_from(rec[:towns])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
private_class_method :_each_record_batch_from
|
|
111
|
+
|
|
112
|
+
# 郵便番号をキーにして CSV の行をまとめます。
|
|
113
|
+
# 都道府県名・市区町村名は同じ郵便番号の最初の行のものを採用します。
|
|
114
|
+
#
|
|
115
|
+
# 例:9896712
|
|
116
|
+
# "宮城県","大崎市","鳴子温泉水沼"
|
|
117
|
+
# "宮城県","大崎市","鳴子温泉南山"
|
|
118
|
+
# "宮城県","大崎市","鳴子温泉山際"
|
|
119
|
+
# "宮城県","大崎市","鳴子温泉和田"
|
|
120
|
+
# これらは
|
|
121
|
+
# "宮城県","大崎市","鳴子温泉" として1つのレコードにします。
|
|
122
|
+
# 共通する地名が抜き出せない場合は空の町名にします。
|
|
123
|
+
#
|
|
124
|
+
# ken_all.csv では町域名が長いと括弧の途中で改行され、次の行に続きます。
|
|
125
|
+
# "米出(124、125、127、"
|
|
126
|
+
# "148、149、154番地)"
|
|
127
|
+
# 続きの行は町域名ではないので、括弧が閉じるまでの行は読み飛ばします。
|
|
128
|
+
def self._read_master_csv(csv_path)
|
|
129
|
+
records = {}
|
|
130
|
+
# 郵便番号ごとの「閉じていない括弧の数」。0 より大きい間は継続行。
|
|
131
|
+
open_parens = Hash.new(0)
|
|
132
|
+
|
|
133
|
+
# ken_all.csv の文字コードは CP932(Ruby では SJIS が CP932 の別名)。
|
|
134
|
+
CSV.foreach(csv_path, encoding: 'CP932:UTF-8') do |row|
|
|
135
|
+
zip = row[2].to_s.strip
|
|
136
|
+
next if zip.empty?
|
|
137
|
+
|
|
138
|
+
raw_town = row[8].to_s
|
|
139
|
+
if open_parens[zip] > 0
|
|
140
|
+
open_parens[zip] += _paren_balance(raw_town)
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
open_parens[zip] = [_paren_balance(raw_town), 0].max
|
|
144
|
+
|
|
145
|
+
record = (records[zip] ||= {
|
|
146
|
+
zip: zip,
|
|
147
|
+
prefecture: row[6].to_s.strip,
|
|
148
|
+
city: row[7].to_s.strip,
|
|
149
|
+
towns: []
|
|
150
|
+
})
|
|
151
|
+
town = _remove_needless_words(raw_town)
|
|
152
|
+
record[:towns] << town if town.present?
|
|
153
|
+
end
|
|
154
|
+
records
|
|
155
|
+
end
|
|
156
|
+
private_class_method :_read_master_csv
|
|
157
|
+
|
|
158
|
+
def self._paren_balance(text)
|
|
159
|
+
text.count('(') - text.count(')')
|
|
160
|
+
end
|
|
161
|
+
private_class_method :_paren_balance
|
|
162
|
+
|
|
163
|
+
# 引数に渡された地名群から、先頭から見て共通となる地名を返します。
|
|
164
|
+
#
|
|
165
|
+
# input = %w[鳴子温泉小身川
|
|
166
|
+
# 鳴子温泉川袋
|
|
167
|
+
# 鳴子温泉木戸脇
|
|
168
|
+
# ]
|
|
169
|
+
#
|
|
170
|
+
# return => 鳴子温泉
|
|
171
|
+
#
|
|
172
|
+
# 共通部分が1文字しかない場合(例:大分/大阪)は、地名として意味を
|
|
173
|
+
# なさないので空文字を返します。ただし短い方の地名がまるごと共通部分に
|
|
174
|
+
# なる場合(例:東中島/東中島本町)は、その地名をそのまま返します。
|
|
175
|
+
def self._find_shared_name_from(names)
|
|
176
|
+
names = Array(names).reject { |name| name.blank? }
|
|
177
|
+
return '' if names.empty?
|
|
178
|
+
|
|
179
|
+
shortest = names.min_by { |name| name.length }
|
|
180
|
+
shared_length = 0
|
|
181
|
+
shortest.each_char.with_index do |char, pos|
|
|
182
|
+
break unless names.all? { |name| name[pos] == char }
|
|
183
|
+
|
|
184
|
+
shared_length = pos + 1
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
return shortest if shared_length == shortest.length
|
|
188
|
+
|
|
189
|
+
shared_length > 1 ? shortest[0, shared_length] : ''
|
|
62
190
|
end
|
|
63
191
|
|
|
64
192
|
def self._clear_table
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
193
|
+
connection_pool.with_connection do |conn|
|
|
194
|
+
begin
|
|
195
|
+
conn.truncate table_name
|
|
196
|
+
rescue NotImplementedError
|
|
197
|
+
conn.delete "delete from #{conn.quote_table_name(table_name)}"
|
|
198
|
+
end
|
|
69
199
|
end
|
|
70
200
|
end
|
|
71
201
|
private_class_method :_clear_table
|
|
72
202
|
|
|
73
203
|
def self._remove_needless_words(base)
|
|
74
|
-
base.sub(/以下に掲載がない場合/, '').sub(/(.*/, '')
|
|
204
|
+
base.to_s.sub(/以下に掲載がない場合/, '').sub(/(.*/, '').strip
|
|
75
205
|
end
|
|
76
206
|
private_class_method :_remove_needless_words
|
|
77
207
|
|
|
78
208
|
def self._remove_csv
|
|
79
|
-
FileUtils.
|
|
209
|
+
FileUtils.rm_f MASTER_CSV_PATH
|
|
80
210
|
end
|
|
81
211
|
private_class_method :_remove_csv
|
|
82
212
|
|
data/config/routes.rb
CHANGED
|
File without changes
|