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,167 @@
|
|
|
1
|
+
# Tasks: CSVインポート時の消えたレコード削除
|
|
2
|
+
|
|
3
|
+
**Input**: Design documents from `/specs/003-csv-remove-obsolete/`
|
|
4
|
+
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/
|
|
5
|
+
|
|
6
|
+
**Tests**: Constitution で TDD 必須のため、各 User Story でテストを先に記載。Red-Green-Refactor で実装する。
|
|
7
|
+
|
|
8
|
+
**Organization**: User Story ごとに Phase を分け、US1 を MVP として独立検証可能にする。
|
|
9
|
+
|
|
10
|
+
## Format: `[ID] [P?] [Story] Description`
|
|
11
|
+
|
|
12
|
+
- **[P]**: 並行実行可能(別ファイル・未完了タスクへの依存なし)
|
|
13
|
+
- **[US1/US2]**: 対応する User Story
|
|
14
|
+
- 各タスクの説明に **ファイルパス** を明記する
|
|
15
|
+
|
|
16
|
+
## Path Conventions
|
|
17
|
+
|
|
18
|
+
- 本リポジトリ: `lib/jp_address_complement/`, `spec/` をリポジトリルート基準で使用
|
|
19
|
+
- Rake: `lib/tasks/jp_address_complement.rake`
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Phase 1: Setup(ベースライン確認)
|
|
24
|
+
|
|
25
|
+
**Purpose**: 003 着手前の既存テスト・Lint が通る状態を確認する
|
|
26
|
+
|
|
27
|
+
- [x] T001 既存の CsvImporter と Rake タスクのテストがパスすることを確認する(spec/importers/csv_importer_spec.rb, spec/tasks/import_task_spec.rb)
|
|
28
|
+
- [x] T002 ベースラインとして bundle exec rubocop がパスすることを確認する
|
|
29
|
+
|
|
30
|
+
**Checkpoint**: 既存インポート仕様が GREEN の状態
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Phase 2: Foundational(全 User Story の前提)
|
|
35
|
+
|
|
36
|
+
**Purpose**: インポート戻り値の型を定義し、US1/US2 の実装で共通利用する
|
|
37
|
+
|
|
38
|
+
**⚠️ CRITICAL**: この Phase 完了まで User Story 実装に着手しない
|
|
39
|
+
|
|
40
|
+
- [x] T003 [P] CsvImporter の import 戻り値用の型(例: Data.define(:upserted, :deleted) または Hash)を lib/jp_address_complement/importers/csv_importer.rb に定義する
|
|
41
|
+
|
|
42
|
+
**Checkpoint**: 戻り値型が定義済み。US1 実装で import の戻り値をこの型に差し替え可能
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Phase 3: User Story 1 - フルインポートで新CSVにない住所レコードを削除する (Priority: P1) 🎯 MVP
|
|
47
|
+
|
|
48
|
+
**Goal**: 今回のCSVに含まれない住所レコードをストアから削除し、CSV とストアの内容を一致させる。
|
|
49
|
+
|
|
50
|
+
**Independent Test**: 既存データがある状態で、一部レコードを除いたCSVでインポートを実行し、除いたレコードがストアから消えていることを確認する。
|
|
51
|
+
|
|
52
|
+
### Tests for User Story 1(先に書き、RED を確認してから実装)
|
|
53
|
+
|
|
54
|
+
- [x] T004 [P] [US1] 新CSVに含まれないレコードが削除されることを spec/importers/csv_importer_spec.rb で検証する(Given 3件存在, When B を除くCSVでインポート, Then A,C のみ残る)
|
|
55
|
+
- [x] T005 [P] [US1] 空CSV(有効行0件)で ImportError が発生し既存データが変更されないことを spec/importers/csv_importer_spec.rb で検証する
|
|
56
|
+
- [x] T006 [P] [US1] 同じCSVで再インポートしても冪等であることを spec/importers/csv_importer_spec.rb で検証する
|
|
57
|
+
- [x] T007 [P] [US1] upsert 途中で失敗した場合は削除フェーズを実行せず既存データが維持されることを spec/importers/csv_importer_spec.rb で検証する(モックまたはスタブで失敗を再現)
|
|
58
|
+
|
|
59
|
+
### Implementation for User Story 1
|
|
60
|
+
|
|
61
|
+
- [x] T008 [US1] CSV 走査中に出現した一意キー (postal_code, pref_code, city, town) を Set で保持する処理を lib/jp_address_complement/importers/csv_importer.rb に追加する
|
|
62
|
+
- [x] T009 [US1] 有効行が1件もない場合に空CSVと判定し JpAddressComplement::ImportError を発生させる処理を lib/jp_address_complement/importers/csv_importer.rb に追加する
|
|
63
|
+
- [x] T010 [US1] upsert が全て成功した後に、キー集合に含まれない行を jp_address_complement_postal_codes から削除する処理を lib/jp_address_complement/importers/csv_importer.rb に追加する
|
|
64
|
+
- [x] T011 [US1] import の戻り値を Phase 2 で定義した型(upserted 件数・deleted 件数)に変更する処理を lib/jp_address_complement/importers/csv_importer.rb に実装する
|
|
65
|
+
|
|
66
|
+
**Checkpoint**: User Story 1 が単体で動作し、削除・空CSV拒否・冪等・失敗時不削除がテストで確認できる状態
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Phase 4: User Story 2 - 削除と追加・更新が同一インポートで正しく反映される (Priority: P2)
|
|
71
|
+
|
|
72
|
+
**Goal**: 追加・更新・削除が一括で正しく反映され、インポート完了時に件数が報告される。
|
|
73
|
+
|
|
74
|
+
**Independent Test**: 既存データに対し、一部削除・一部追加・一部変更したCSVでインポートし、最終状態がCSVと一致し、件数が表示されることを確認する。
|
|
75
|
+
|
|
76
|
+
### Tests for User Story 2
|
|
77
|
+
|
|
78
|
+
- [x] T012 [P] [US2] 同一インポートで削除・追加・更新が正しく反映される統合テストを spec/importers/csv_importer_spec.rb に追加する(例: A 削除・C 追加 → B,C のみ残る)
|
|
79
|
+
- [x] T013 [P] [US2] Rake タスク実行時に標準出力に upserted/deleted 件数が含まれることを spec/tasks/import_task_spec.rb で検証する
|
|
80
|
+
|
|
81
|
+
### Implementation for User Story 2
|
|
82
|
+
|
|
83
|
+
- [x] T014 [US2] jp_address_complement:import タスクで CsvImporter#import の戻り値を受け取り、標準出力に「インポート完了: upsert N 件, 削除 M 件」を表示する処理を lib/tasks/jp_address_complement.rake に追加する
|
|
84
|
+
|
|
85
|
+
**Checkpoint**: User Story 1 と 2 がともに動作し、Rake から件数が確認できる状態
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Phase 5: Polish & Cross-Cutting Concerns
|
|
90
|
+
|
|
91
|
+
**Purpose**: 品質ゲートとドキュメントの最終確認
|
|
92
|
+
|
|
93
|
+
- [x] T015 [P] 全テストが GREEN であることと bundle exec rubocop がパスすることを確認する
|
|
94
|
+
- [x] T016 [P] SimpleCov でカバレッジ 90% 以上を確認する
|
|
95
|
+
- [x] T017 公開 API 変更(CsvImporter#import の戻り値が Integer → 件数オブジェクト)に合わせて CHANGELOG を更新する(Constitution Quality Gates 準拠)
|
|
96
|
+
- [x] T018 quickstart.md の手順(インポートで消えたレコード削除・空CSV拒否・件数表示)を手動で確認し、必要なら docs を更新する
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Dependencies & Execution Order
|
|
101
|
+
|
|
102
|
+
### Phase Dependencies
|
|
103
|
+
|
|
104
|
+
- **Phase 1 (Setup)**: 依存なし。即時開始可能。
|
|
105
|
+
- **Phase 2 (Foundational)**: Phase 1 完了後に開始。全 User Story の前提となる。
|
|
106
|
+
- **Phase 3 (US1)**: Phase 2 完了後に開始。MVP。
|
|
107
|
+
- **Phase 4 (US2)**: Phase 3 完了後に開始。US1 の拡張(件数報告・Rake 出力)。
|
|
108
|
+
- **Phase 5 (Polish)**: Phase 4 完了後に実施。T017(CHANGELOG)は Constitution Quality Gates 必須。
|
|
109
|
+
|
|
110
|
+
### User Story Dependencies
|
|
111
|
+
|
|
112
|
+
- **User Story 1 (P1)**: Phase 2 完了後から開始可能。他ストーリーに依存しない。
|
|
113
|
+
- **User Story 2 (P2)**: Phase 3 完了後から開始可能。US1 の import 戻り値を Rake が表示するだけなので、US1 実装に依存。
|
|
114
|
+
|
|
115
|
+
### Within Each User Story
|
|
116
|
+
|
|
117
|
+
- テストを先に書き、失敗を確認してから実装(TDD)。
|
|
118
|
+
- T004〜T007 が RED の状態で T008〜T011 を実装して GREEN にする。
|
|
119
|
+
- T012〜T013 が RED の状態で T014 を実装して GREEN にする。
|
|
120
|
+
|
|
121
|
+
### Parallel Opportunities
|
|
122
|
+
|
|
123
|
+
- Phase 1: T001 と T002 は順不同(T002 は Lint のみ)。
|
|
124
|
+
- Phase 2: T003 のみ。
|
|
125
|
+
- Phase 3: T004, T005, T006, T007 は [P] のため並行してテストを記述可能。T008〜T011 は順序あり(キー保持→空CSV→削除→戻り値)。
|
|
126
|
+
- Phase 4: T012 と T013 は [P] で並行してテスト記述可能。
|
|
127
|
+
- Phase 5: T015 と T016 は [P] で並行可能。T017(CHANGELOG)と T018(quickstart 確認)は順不同で実施。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Parallel Example: User Story 1
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
# テストを並行して追加(いずれも spec/importers/csv_importer_spec.rb):
|
|
135
|
+
T004: 新CSVにないレコード削除の example
|
|
136
|
+
T005: 空CSVで ImportError の example
|
|
137
|
+
T006: 冪等の example
|
|
138
|
+
T007: upsert 失敗時は削除しない example
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Implementation Strategy
|
|
144
|
+
|
|
145
|
+
### MVP First(User Story 1 のみ)
|
|
146
|
+
|
|
147
|
+
1. Phase 1 完了 → ベースライン確認
|
|
148
|
+
2. Phase 2 完了 → 戻り値型定義
|
|
149
|
+
3. Phase 3 完了 → 削除・空CSV拒否・冪等・戻り値
|
|
150
|
+
4. **STOP and VALIDATE**: CsvImporter のテストのみで US1 を検証
|
|
151
|
+
5. 必要ならデモまたはコミット
|
|
152
|
+
|
|
153
|
+
### Incremental Delivery
|
|
154
|
+
|
|
155
|
+
1. Phase 1 + 2 → 基盤のみ
|
|
156
|
+
2. Phase 3 (US1) → 削除機能が単体で動作(MVP)
|
|
157
|
+
3. Phase 4 (US2) → 件数報告と Rake 出力
|
|
158
|
+
4. Phase 5 → 品質ゲートと quickstart 確認
|
|
159
|
+
|
|
160
|
+
### Notes
|
|
161
|
+
|
|
162
|
+
- [P] タスクは別ファイル・依存なしで並行可能。
|
|
163
|
+
- [US1]/[US2] でストーリーとタスクを対応付ける。
|
|
164
|
+
- 各 User Story は独立して完了・検証可能。
|
|
165
|
+
- テストは必ず先に書き、失敗を確認してから実装する。
|
|
166
|
+
- チェックポイントでストーリー単位の検証を行う。
|
|
167
|
+
- **SC-003(許容可能な範囲)**: パフォーマンスは plan 上の目安(既存インポート時間の約2倍以内)とする。本 tasks では専用の計測・検証タスクは設けず、必要に応じて手動またはベンチで確認する。
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Specification Quality Checklist: 都道府県コード変換・住所逆引き(住所→郵便番号)
|
|
2
|
+
|
|
3
|
+
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
|
4
|
+
**Created**: 2026-02-23
|
|
5
|
+
**Feature**: [spec.md](../spec.md)
|
|
6
|
+
|
|
7
|
+
## Content Quality
|
|
8
|
+
|
|
9
|
+
- [x] No implementation details (languages, frameworks, APIs)
|
|
10
|
+
- [x] Focused on user value and business needs
|
|
11
|
+
- [x] Written for non-technical stakeholders
|
|
12
|
+
- [x] All mandatory sections completed
|
|
13
|
+
|
|
14
|
+
## Requirement Completeness
|
|
15
|
+
|
|
16
|
+
- [x] No [NEEDS CLARIFICATION] markers remain
|
|
17
|
+
- [x] Requirements are testable and unambiguous
|
|
18
|
+
- [x] Success criteria are measurable
|
|
19
|
+
- [x] Success criteria are technology-agnostic (no implementation details)
|
|
20
|
+
- [x] All acceptance scenarios are defined
|
|
21
|
+
- [x] Edge cases are identified
|
|
22
|
+
- [x] Scope is clearly bounded
|
|
23
|
+
- [x] Dependencies and assumptions identified
|
|
24
|
+
|
|
25
|
+
## Feature Readiness
|
|
26
|
+
|
|
27
|
+
- [x] All functional requirements have clear acceptance criteria
|
|
28
|
+
- [x] User scenarios cover primary flows
|
|
29
|
+
- [x] Feature meets measurable outcomes defined in Success Criteria
|
|
30
|
+
- [x] No implementation details leak into specification
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- すべてのチェック項目をクリアしています。`/speckit.clarify` または `/speckit.plan` へ進んでください。
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Public API Contract: 都道府県コード変換・住所逆引き(004 追加 API)
|
|
2
|
+
|
|
3
|
+
**Gem**: `jp_address_complement`
|
|
4
|
+
**Namespace**: `JpAddressComplement`
|
|
5
|
+
**Contract Type**: Ruby Public API(004 で追加する公開インターフェース)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 追加するモジュールメソッド
|
|
10
|
+
|
|
11
|
+
既存の `search_by_postal_code` / `search_by_postal_code_prefix` / `valid_combination?` に加え、以下 3 つを提供する。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### `prefecture_name_from_code(code) → String | nil`
|
|
16
|
+
|
|
17
|
+
都道府県コード(JIS X 0401、2桁)から都道府県名を返す。
|
|
18
|
+
|
|
19
|
+
**引数**
|
|
20
|
+
|
|
21
|
+
| 名前 | 型 | 説明 |
|
|
22
|
+
|-----|----|------|
|
|
23
|
+
| `code` | `String`, `Integer`, `nil` | 都道府県コード。数値(例: 13)またはゼロパディング文字列(例: "13", "01")。 |
|
|
24
|
+
|
|
25
|
+
**戻り値**: `String` または `nil`
|
|
26
|
+
|
|
27
|
+
- 有効なコード(01–47)の場合は対応する都道府県名(例: "東京都")。
|
|
28
|
+
- 存在しないコード・範囲外・nil・空文字の場合は `nil`。例外は発生しない。
|
|
29
|
+
|
|
30
|
+
**例**
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
JpAddressComplement.prefecture_name_from_code("13") # => "東京都"
|
|
34
|
+
JpAddressComplement.prefecture_name_from_code(13) # => "東京都"
|
|
35
|
+
JpAddressComplement.prefecture_name_from_code("01") # => "北海道"
|
|
36
|
+
JpAddressComplement.prefecture_name_from_code(99) # => nil
|
|
37
|
+
JpAddressComplement.prefecture_name_from_code(nil) # => nil
|
|
38
|
+
JpAddressComplement.prefecture_name_from_code("") # => nil
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### `prefecture_code_from_name(name) → String | nil`
|
|
44
|
+
|
|
45
|
+
都道府県名(正式名称)から都道府県コードを 2 桁の文字列で返す。
|
|
46
|
+
|
|
47
|
+
**引数**
|
|
48
|
+
|
|
49
|
+
| 名前 | 型 | 説明 |
|
|
50
|
+
|-----|----|------|
|
|
51
|
+
| `name` | `String`, `nil` | 都道府県の正式名称(例: "東京都", "北海道")。省略表記は受け付けない。 |
|
|
52
|
+
|
|
53
|
+
**戻り値**: `String` または `nil`
|
|
54
|
+
|
|
55
|
+
- 正式名称に一致する場合は 2 桁の文字列(例: "13", "01")。ゼロパディングを維持する。
|
|
56
|
+
- 一致しない場合・nil・空文字の場合は `nil`。例外は発生しない。
|
|
57
|
+
|
|
58
|
+
**例**
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
JpAddressComplement.prefecture_code_from_name("東京都") # => "13"
|
|
62
|
+
JpAddressComplement.prefecture_code_from_name("北海道") # => "01"
|
|
63
|
+
JpAddressComplement.prefecture_code_from_name("東京") # => nil(省略表記は不可)
|
|
64
|
+
JpAddressComplement.prefecture_code_from_name("不明") # => nil
|
|
65
|
+
JpAddressComplement.prefecture_code_from_name(nil) # => nil
|
|
66
|
+
JpAddressComplement.prefecture_code_from_name("") # => nil
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### `search_postal_codes_by_address(pref:, city:, town: nil) → Array<String>`
|
|
72
|
+
|
|
73
|
+
都道府県・市区町村・町域(任意)を指定し、該当する郵便番号の候補一覧を返す(逆引き)。
|
|
74
|
+
|
|
75
|
+
**引数**
|
|
76
|
+
|
|
77
|
+
| 名前 | 型 | 必須 | 説明 |
|
|
78
|
+
|-----|----|------|------|
|
|
79
|
+
| `pref` | `String` | ✅ | 都道府県名(正式名称)。住所データの `pref` と完全一致。 |
|
|
80
|
+
| `city` | `String` | ✅ | 市区町村名。住所データの `city` と完全一致。 |
|
|
81
|
+
| `town` | `String` | ❌ | 町域名。省略時は都道府県+市区町村のみで検索。指定時は完全一致で絞り込む。 |
|
|
82
|
+
|
|
83
|
+
**戻り値**: `Array<String>`
|
|
84
|
+
|
|
85
|
+
- 該当する郵便番号(7桁文字列)の配列。重複は除く。
|
|
86
|
+
- 該当なし・pref または city が nil/空・入力不十分の場合は `[]`。例外は発生しない。
|
|
87
|
+
|
|
88
|
+
**照合**: 各フィールドは既存の住所データ(`pref`, `city`, `town`)と **完全一致**。入力はデータと同じ表記を前提とする。
|
|
89
|
+
|
|
90
|
+
**例**
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# 都道府県・市区町村・町域を指定
|
|
94
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: "千代田区", town: "千代田")
|
|
95
|
+
# => ["1000001", ...]
|
|
96
|
+
|
|
97
|
+
# 町域は省略(都道府県+市区町村のみ)
|
|
98
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: "千代田区")
|
|
99
|
+
# => ["1000001", "1000002", ...]
|
|
100
|
+
|
|
101
|
+
# 該当なし
|
|
102
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: "存在しない区")
|
|
103
|
+
# => []
|
|
104
|
+
|
|
105
|
+
# 入力不十分(検索しない)
|
|
106
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: nil)
|
|
107
|
+
# => []
|
|
108
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: "")
|
|
109
|
+
# => []
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Repository の拡張(内部契約)
|
|
115
|
+
|
|
116
|
+
`JpAddressComplement::Repositories::PostalCodeRepository` に以下を追加する。
|
|
117
|
+
|
|
118
|
+
### `find_postal_codes_by_address(pref:, city:, town: nil) → Array<String>`
|
|
119
|
+
|
|
120
|
+
- 都道府県・市区町村・町域で完全一致検索し、郵便番号の配列を返す。
|
|
121
|
+
- pref または city が nil/空の場合は `[]` を返す。
|
|
122
|
+
- 実装は `ActiveRecordPostalCodeRepository` で、既存の `postal_codes` テーブルに対して `WHERE pref = ? AND city = ? AND (town = ? OR town IS NULL)` に相当するクエリを発行し、結果の postal_code を重複排除して返す。
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Data Model: 都道府県コード変換・住所逆引き
|
|
2
|
+
|
|
3
|
+
**Branch**: `004-prefecture-code-reverse-lookup`
|
|
4
|
+
**Date**: 2026-02-23
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 概要
|
|
9
|
+
|
|
10
|
+
本機能では **新規の DB テーブルは追加しない**。都道府県コード⇔名称はメモリ内の定数、逆引きは既存の `jp_address_complement_postal_codes` テーブルを利用する。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. 都道府県マッピング(論理エンティティ)
|
|
15
|
+
|
|
16
|
+
コード⇔名称の対応は、gem 内の静的なデータとして保持する。
|
|
17
|
+
|
|
18
|
+
### 属性
|
|
19
|
+
|
|
20
|
+
| 属性 | 型 | 説明 |
|
|
21
|
+
|------|-----|------|
|
|
22
|
+
| コード | String (2桁) | JIS X 0401。01–47。ゼロパディング("01", "13")。 |
|
|
23
|
+
| 名称 | String | 都道府県名(正式名称)。例: "東京都", "北海道"。 |
|
|
24
|
+
|
|
25
|
+
### 制約
|
|
26
|
+
|
|
27
|
+
- コードは 01–47 の範囲のみ有効。それ以外は該当なしとして nil を返す。
|
|
28
|
+
- 名称は正式名称のみ有効。省略表記は受け付けない(該当なし → nil)。
|
|
29
|
+
- 入力が nil または空文字の場合は例外を投げず nil を返す。
|
|
30
|
+
|
|
31
|
+
### 実装上の表現
|
|
32
|
+
|
|
33
|
+
- Ruby ではハッシュ 2 つ、または 1 つのハッシュで双方向参照可能にする。
|
|
34
|
+
- 例: `CODE_TO_NAME = { "01" => "北海道", ... }` と `NAME_TO_CODE = CODE_TO_NAME.invert`(または 47 件の定数マッピング)。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. 逆引き入力(Reverse Lookup Input)
|
|
39
|
+
|
|
40
|
+
API の引数として渡す構造。DB の形ではなく、メソッドのキーワード引数として表現する。
|
|
41
|
+
|
|
42
|
+
### フィールド
|
|
43
|
+
|
|
44
|
+
| フィールド | 型 | 必須 | 説明 |
|
|
45
|
+
|-----------|-----|------|------|
|
|
46
|
+
| pref | String | ✅ | 都道府県名(正式名称)。住所データの `pref` と完全一致。 |
|
|
47
|
+
| city | String | ✅ | 市区町村名。住所データの `city` と完全一致。 |
|
|
48
|
+
| town | String | ❌ | 町域名。省略時は都道府県+市区町村のみで検索。指定時は `town` と完全一致で絞り込む。 |
|
|
49
|
+
|
|
50
|
+
### 検索条件
|
|
51
|
+
|
|
52
|
+
- pref と city が両方揃っている場合のみ検索を実行する。
|
|
53
|
+
- 各フィールドは既存の `jp_address_complement_postal_codes` の `pref`, `city`, `town` と **完全一致** で照合する。
|
|
54
|
+
- 入力は UTF-8。住所データ(郵便番号データ)と同じ表記を前提とする。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. 逆引き結果(郵便番号候補)
|
|
59
|
+
|
|
60
|
+
逆引きの戻り値は **郵便番号(7桁文字列)の配列**。
|
|
61
|
+
|
|
62
|
+
### 制約
|
|
63
|
+
|
|
64
|
+
- 該当する郵便番号を重複なく返す。同一郵便番号に複数レコード(町域違い等)がある場合は 1 つにまとめる。
|
|
65
|
+
- 該当なし・入力不十分(pref または city が空など)の場合は空配列 `[]` を返す。
|
|
66
|
+
- 不正入力時も例外を投げず `[]` を返す。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. 既存テーブルとの関係
|
|
71
|
+
|
|
72
|
+
既存の `jp_address_complement_postal_codes` のカラム `pref_code`, `pref`, `city`, `town` をそのまま利用する。
|
|
73
|
+
|
|
74
|
+
### 逆引き用のインデックス(推奨)
|
|
75
|
+
|
|
76
|
+
パフォーマンス要件(SC-003: 50ms p99)を満たすため、次の複合インデックスの追加を推奨する。
|
|
77
|
+
|
|
78
|
+
- カラム: `(pref, city, town)` または `(pref_code, city, town)`
|
|
79
|
+
- 用途: `WHERE pref = ? AND city = ? AND (town = ? OR town IS NULL)` のような逆引きクエリの高速化
|
|
80
|
+
|
|
81
|
+
既存の 001 マイグレーションを変更しない方針とする場合は、別マイグレーションまたはドキュメント上の「推奨インデックス」として案内する。
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Implementation Plan: 都道府県コード変換・住所逆引き(住所→郵便番号)
|
|
2
|
+
|
|
3
|
+
**Branch**: `004-prefecture-code-reverse-lookup` | **Date**: 2026-02-23 | **Spec**: [spec.md](./spec.md)
|
|
4
|
+
**Input**: Feature specification from `/specs/004-prefecture-code-reverse-lookup/spec.md`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
都道府県コード⇔都道府県名の相互変換と、住所(都道府県・市区町村・町域を分離した入力)から郵便番号候補を取得する逆引き機能を追加する。
|
|
11
|
+
**技術方針**: コード⇔名称は JIS X 0401(01–47)の静的なマッピングで実装し、DB 不要で即時応答する。逆引きは既存の `PostalCodeRepository` に `find_postal_codes_by_address(pref:, city:, town:)` を追加し、既存の `postal_codes` テーブルに対して都道府県・市区町村・町域の完全一致で検索する。既存の Searcher / モジュールメソッドと同様に、Repository 経由で Rails 非依存のコアロジックを保つ。
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Technical Context
|
|
16
|
+
|
|
17
|
+
**Language/Version**: Ruby 3.0+(3.2 以上推奨)
|
|
18
|
+
**Primary Dependencies**: Rails 7.x 以上(オプション)、RSpec、SimpleCov、RuboCop
|
|
19
|
+
**Storage**: 都道府県コード⇔名称はメモリ内の定数マッピング。逆引きは既存の `jp_address_complement_postal_codes` テーブルを利用。
|
|
20
|
+
**Testing**: RSpec(カバレッジ 90% 以上、SimpleCov 計測)
|
|
21
|
+
**Target Platform**: Ruby gem(Rails 組み込み時は Railtie 経由)
|
|
22
|
+
**Project Type**: Ruby gem(ライブラリ)
|
|
23
|
+
**Performance Goals**:
|
|
24
|
+
- 都道府県コード⇔名称: < 5ms(定数参照のため trivial)
|
|
25
|
+
- 逆引き: < 50ms p99(既存 DB インデックス活用、後述の複合インデックス追加を推奨)
|
|
26
|
+
**Constraints**: Rubocop 100% PASS(disable 禁止)、TDD 必須
|
|
27
|
+
**Scale/Scope**: 47 都道府県のマッピング、逆引きは既存の約 12 万件の郵便番号データに対して完全一致検索。
|
|
28
|
+
**Encoding**: 入力・出力とも UTF-8。既存仕様と同一。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Constitution Check
|
|
33
|
+
|
|
34
|
+
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
|
35
|
+
|
|
36
|
+
- [x] **I. Gem-First**: 機能はすべて `JpAddressComplement` 名前空間に閉じる。都道府県マッピングは gem 内の定数、逆引きは既存 Repository の拡張のみ。責務は「住所・郵便番号の補完・変換」の範囲内。
|
|
37
|
+
- [x] **II. TDD**: 新機能に対し先にテストを書き、Red-Green-Refactor を厳守。既存の FakeRepository パターンを拡張して逆引きをテスト可能にする。
|
|
38
|
+
- [x] **III. Rubocop**: 新規・変更コードは `rubocop:disable` なしで 100% パス。
|
|
39
|
+
- [x] **IV. データ整合性**: 新規テーブルは追加しない。既存の郵便番号データと JIS X 0401 の都道府県コード体系を一致させて使用。インポート戦略は変更なし。
|
|
40
|
+
- [x] **V. 機能要件**: 都道府県コード→名称・名称→コード・逆引きの 3 機能を明確なインターフェースで提供。該当なし時は仕様どおりコード⇔名称は `nil`、逆引きは `[]`。不正入力は例外を投げず `nil` / `[]` で応答。
|
|
41
|
+
- [x] **VI. シンプルさ**: 都道府県マッピングは 47 件の静的なハッシュ/定数で足りる(YAGNI)。逆引きは既存 Repository に 1 メソッド追加で対応。
|
|
42
|
+
|
|
43
|
+
**Constitution Check Result**: ✅ 全ゲート PASS
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Project Structure
|
|
48
|
+
|
|
49
|
+
### Documentation (this feature)
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
specs/004-prefecture-code-reverse-lookup/
|
|
53
|
+
├── plan.md # このファイル
|
|
54
|
+
├── spec.md # 機能仕様
|
|
55
|
+
├── research.md # Phase 0 調査
|
|
56
|
+
├── data-model.md # Phase 1 データモデル
|
|
57
|
+
├── quickstart.md # Phase 1 利用者向けガイド
|
|
58
|
+
├── contracts/ # Phase 1 API コントラクト
|
|
59
|
+
│ └── public-api-prefecture-and-reverse.md
|
|
60
|
+
└── tasks.md # (/speckit.tasks で生成)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Source Code (repository root)
|
|
64
|
+
|
|
65
|
+
既存構造を拡張するのみ。新規ディレクトリは作らない。
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
lib/jp_address_complement/
|
|
69
|
+
├── jp_address_complement.rb # モジュールメソッド 3 つ追加(後述)
|
|
70
|
+
├── searcher.rb # 逆引きメソッド 1 つ追加
|
|
71
|
+
├── prefecture.rb # 新規: 都道府県コード⇔名称(定数+メソッド)
|
|
72
|
+
├── repositories/
|
|
73
|
+
│ ├── postal_code_repository.rb # find_postal_codes_by_address を追加
|
|
74
|
+
│ └── active_record_postal_code_repository.rb # 上記の実装
|
|
75
|
+
└── (その他既存ファイルは変更なし)
|
|
76
|
+
|
|
77
|
+
spec/
|
|
78
|
+
├── jp_address_complement/
|
|
79
|
+
│ └── prefecture_spec.rb # 新規: 都道府県変換のユニットテスト
|
|
80
|
+
├── searcher_spec.rb # 逆引きのテストを追加
|
|
81
|
+
├── repositories/
|
|
82
|
+
│ └── active_record_postal_code_repository_spec.rb # find_postal_codes_by_address のテスト
|
|
83
|
+
└── (既存テストは必要に応じて拡張)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Structure Decision**: 既存の gem レイアウトを維持。都道府県ロジックは `Prefecture` モジュール(定数+クラスメソッド)に集約し、逆引きは `Searcher` と `PostalCodeRepository` に追加する。
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Complexity Tracking
|
|
91
|
+
|
|
92
|
+
(本機能では Constitution 違反はなく、記録不要)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# クイックスタート: 都道府県コード変換・住所逆引き(004)
|
|
2
|
+
|
|
3
|
+
**前提**: 001 のセットアップ(マイグレーション・郵便番号データのインポート)が完了していること。
|
|
4
|
+
逆引き機能は既存の `jp_address_complement_postal_codes` テーブルを利用します。都道府県コード⇔名称は DB 不要で利用できます。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 都道府県コードから都道府県名を取得する
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# 2桁の文字列(ゼロパディング)
|
|
12
|
+
JpAddressComplement.prefecture_name_from_code("13")
|
|
13
|
+
# => "東京都"
|
|
14
|
+
|
|
15
|
+
JpAddressComplement.prefecture_name_from_code("01")
|
|
16
|
+
# => "北海道"
|
|
17
|
+
|
|
18
|
+
# 数値も受け付ける
|
|
19
|
+
JpAddressComplement.prefecture_name_from_code(13)
|
|
20
|
+
# => "東京都"
|
|
21
|
+
|
|
22
|
+
# 該当しないコード・nil・空文字の場合は nil(例外なし)
|
|
23
|
+
JpAddressComplement.prefecture_name_from_code(99) # => nil
|
|
24
|
+
JpAddressComplement.prefecture_name_from_code(nil) # => nil
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 都道府県名から都道府県コードを取得する
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# 正式名称のみ有効(省略表記は不可)
|
|
33
|
+
JpAddressComplement.prefecture_code_from_name("東京都")
|
|
34
|
+
# => "13"
|
|
35
|
+
|
|
36
|
+
JpAddressComplement.prefecture_code_from_name("北海道")
|
|
37
|
+
# => "01"
|
|
38
|
+
|
|
39
|
+
# 戻り値は常に 2 桁の文字列(ゼロパディング維持)
|
|
40
|
+
JpAddressComplement.prefecture_code_from_name("北海道")
|
|
41
|
+
# => "01"
|
|
42
|
+
|
|
43
|
+
# 該当しない名称・省略表記・nil・空の場合は nil
|
|
44
|
+
JpAddressComplement.prefecture_code_from_name("東京") # => nil
|
|
45
|
+
JpAddressComplement.prefecture_code_from_name(nil) # => nil
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 住所から郵便番号を検索する(逆引き)
|
|
51
|
+
|
|
52
|
+
都道府県・市区町村・町域を **分離した引数** で渡します。都道府県と市区町村は必須、町域は任意です。各フィールドは住所データと **完全一致** で照合されます(データと同じ表記で渡してください)。
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# 都道府県・市区町村・町域を指定
|
|
56
|
+
JpAddressComplement.search_postal_codes_by_address(
|
|
57
|
+
pref: "東京都",
|
|
58
|
+
city: "千代田区",
|
|
59
|
+
town: "千代田"
|
|
60
|
+
)
|
|
61
|
+
# => ["1000001", ...]
|
|
62
|
+
|
|
63
|
+
# 町域は省略(都道府県+市区町村に属する郵便番号をすべて返す)
|
|
64
|
+
JpAddressComplement.search_postal_codes_by_address(
|
|
65
|
+
pref: "東京都",
|
|
66
|
+
city: "千代田区"
|
|
67
|
+
)
|
|
68
|
+
# => ["1000001", "1000002", ...]
|
|
69
|
+
|
|
70
|
+
# 該当なし・入力不十分の場合は空配列(例外なし)
|
|
71
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: "存在しない区")
|
|
72
|
+
# => []
|
|
73
|
+
|
|
74
|
+
JpAddressComplement.search_postal_codes_by_address(pref: "東京都", city: nil)
|
|
75
|
+
# => []
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## パフォーマンス(逆引き)
|
|
81
|
+
|
|
82
|
+
逆引きの応答時間を 50ms 以下に保つため、次の複合インデックスの追加を推奨します(未追加の場合は data-model.md を参照)。
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# マイグレーション例(推奨)
|
|
86
|
+
add_index :jp_address_complement_postal_codes,
|
|
87
|
+
[:pref, :city, :town],
|
|
88
|
+
name: "idx_jp_address_complement_reverse_lookup"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
既存の郵便番号テーブルに上記インデックスがない場合、データ量によってはクエリが遅くなる可能性があります。
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Research: 都道府県コード変換・住所逆引き
|
|
2
|
+
|
|
3
|
+
**Branch**: `004-prefecture-code-reverse-lookup`
|
|
4
|
+
**Date**: 2026-02-23
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 都道府県コード⇔名称の保持場所
|
|
9
|
+
|
|
10
|
+
**Decision**: 47 都道府県のコード・名称マッピングは **gem 内の静的な定数(ハッシュまたは配列)** で保持する。DB や外部ファイルは使わない。
|
|
11
|
+
|
|
12
|
+
**Rationale**:
|
|
13
|
+
- JIS X 0401 の都道府県コード(01–47)は国で定められた固定データであり、郵便番号データの更新とは独立している。
|
|
14
|
+
- 47 件のみのためメモリ負荷は無視でき、参照も O(1) で 5ms 以下の応答を満たせる。
|
|
15
|
+
- DB に依存しないため、未インポート状態でもコード⇔名称だけは利用可能になり、利用者にとって分かりやすい。
|
|
16
|
+
- 既存の `postal_codes` テーブルの `pref_code` / `pref` と体系を揃えるため、JIS X 0401 の正式な 01–47 のマッピングを採用する。
|
|
17
|
+
|
|
18
|
+
**Alternatives considered**:
|
|
19
|
+
- **DB の distinct から取得**: 郵便番号データに依存し、未インポート時は使えない。また毎回クエリする必要があり、定数参照より遅い。
|
|
20
|
+
- **外部 YAML/JSON**: ファイル I/O とパースが発生し、定数より遅く、配布・バージョン管理も複雑になる。
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. 逆引きの実装経路(Repository 拡張)
|
|
25
|
+
|
|
26
|
+
**Decision**: 既存の `PostalCodeRepository` に **`find_postal_codes_by_address(pref:, city:, town: nil)`** を追加する。戻り値は郵便番号の配列(`Array<String>` または仕様に合わせて `Array<AddressRecord>` の postal_code のみ返すかは contracts で定義)。実装は `ActiveRecordPostalCodeRepository` で `WHERE pref = ? AND city = ? AND (town = ? OR town IS NULL)` のような完全一致クエリとする。
|
|
27
|
+
|
|
28
|
+
**Rationale**:
|
|
29
|
+
- 既存の「郵便番号→住所」と対になる逆方向の検索であり、同じ Repository に置くことで一貫したデータアクセス層を保てる。
|
|
30
|
+
- 仕様で「都道府県+市区町村必須、町域は任意」「各フィールド完全一致」とあるため、1 メソッドで表現できる。
|
|
31
|
+
- Rails 非依存のコアを保つため、Repository インターフェースにだけ依存し、Searcher がそれを呼ぶ形は既存パターンと同じ。
|
|
32
|
+
|
|
33
|
+
**Alternatives considered**:
|
|
34
|
+
- **専用の ReverseLookupRepository**: 現時点では 1 クエリで完結するため、YAGNI の観点で既存 Repository の拡張で十分。
|
|
35
|
+
- **連結文字列で検索**: 仕様で分離入力・完全一致に決まっているため不採用。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 3. 逆引きクエリのインデックス
|
|
40
|
+
|
|
41
|
+
**Decision**: 逆引きのパフォーマンス(SC-003: 50ms p99)を満たすため、**`(pref, city, town)` の複合インデックス**(または `(pref_code, city, town)`)を推奨する。既存の `postal_code` 単一インデックスと一意制約 `(postal_code, pref_code, city, town)` だけでは、逆引きはフルスキャンに近くなる可能性がある。
|
|
42
|
+
|
|
43
|
+
**Rationale**:
|
|
44
|
+
- 逆引きは `WHERE pref = ? AND city = ? AND (town = ? OR town IS NULL)` のような条件になるため、`pref, city, town` の順の複合インデックスが有効。
|
|
45
|
+
- 既存テーブルに対する追加インデックスは、マイグレーションまたは quickstart で「推奨インデックス」として案内する。001 で既存のマイグレーションに含めていないため、004 で「逆引き用推奨インデックス」をドキュメントと optional マイグレーションで提供するか、既存マイグレーションを変更しない場合は data-model と quickstart に記載のみとする(実装時に判断)。
|
|
46
|
+
|
|
47
|
+
**Alternatives considered**:
|
|
48
|
+
- **インデックスなし**: 12 万件程度ならフルスキャンでも 50ms を下回る可能性はあるが、保証は難しい。インデックスを推奨する方が安全。
|
|
49
|
+
- **全文検索**: 仕様は完全一致のため不要。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 4. 戻り値の型(逆引き)
|
|
54
|
+
|
|
55
|
+
**Decision**: 逆引きの戻り値は **郵便番号の文字列の配列 `Array<String>`** とする。`AddressRecord` の配列でも要件は満たせるが、仕様が「郵便番号の候補一覧」としているため、呼び出し側がそのまま使える `["1000001", "1000002"]` の形を採用する。必要なら後から `Array<AddressRecord>` を返す API を追加可能。
|
|
56
|
+
|
|
57
|
+
**Rationale**:
|
|
58
|
+
- 仕様の「郵便番号の候補一覧」に素直に対応し、既存の `search_by_postal_code`(AddressRecord 配列)とは用途が違うため、シンプルに文字列配列で揃える。
|
|
59
|
+
- 一意化は仕様どおり「該当するすべての郵便番号を候補として返す」で、同一郵便番号が複数レコード(町域違い)で存在する場合は 1 つにまとめる。
|
|
60
|
+
|
|
61
|
+
**Alternatives considered**:
|
|
62
|
+
- **Array<AddressRecord>**: 将来的に住所詳細も必要になった場合の拡張用。現仕様では「郵便番号候補」のみなので、まずは `Array<String>` で十分。
|