Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Shift_JIS のテスト用データは改行・文字コードを変換しない
ZipConvertCustomerBarCode.Tests/TestData/jigyosyo_sample.csv binary
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@

## 構成・ビルド

- ライブラリは 3 つ、すべて `netstandard2.0`(SDK スタイル)。ビルドは `dotnet build ZipConvertCustomerBarCode.sln`。
- `ZipConvertCustomerBarCode`: コア。外部依存を追加しない(描画ライブラリに依存させないのが分割の目的)。
- ライブラリは 3 つ(SDK スタイル)。ビルドは `dotnet build ZipConvertCustomerBarCode.sln`。
- `ZipConvertCustomerBarCode`: コア。`netstandard2.0` と `net8.0`。外部依存を追加しない(描画ライブラリに依存させないのが分割の目的。`System.Memory` も入れない)。
- `Span<T>` などは `#if NET8_0_OR_GREATER` の中だけで使い、`#else` に `netstandard2.0` 用の処理を書く。`#if` は小さな範囲(メソッドや構造体の中)に閉じ込め、共通にできる部分は共通のメソッドにする。
- `Span` を使うのは計測で効果があった箇所のみ(住所照合の正規化、CSV の読み込み)。`Span` を使わなくても両方で速くできる場合は、`#if` を使わず共通のコードにする。
- `ZipConvertCustomerBarCode.Drawing`: System.Drawing.Common による描画(Windows のみ)。
- `ZipConvertCustomerBarCode.SkiaSharp`: SkiaSharp による描画。名前空間は `SkiaSharp` との衝突を避けるため `ZipConvertCustomerBarCode.Skia`。
- `netstandard2.0` の既定 C# は 7.3。`record` や null 許容参照型など新しい構文は使わない。
- `netstandard2.0` の既定 C# は 7.3。`#if NET8_0_OR_GREATER` の外では `record` や null 許容参照型など新しい構文は使わない。
- バージョン・著作権表示は `Directory.Build.props` にまとめている。
- 公開メンバーには XML ドキュメントコメントを付ける(`GenerateDocumentationFile` 有効のため、ないと警告になる)。

## テスト

- `dotnet test` で実行(xUnit v3、net10.0)。`global.json` で Microsoft.Testing.Platform を指定しているため、`Microsoft.NET.Test.Sdk` や `xunit.runner.visualstudio` は追加しない。
- コアは `#if` で処理が分かれるため、変更したら両方のビルドでテストする: `dotnet test`(net8.0 版)と `dotnet test -p:CoreTargetFramework=netstandard2.0`。`TargetFrameworkTests` が、指定したビルドが実際に使われていることを確認する。
- `TestData/cases.tsv` と `v1-*.png` は 1.x(.NET Framework 版)の出力で、シンボル列と画像がこれとピクセル単位で一致することを確認している。1.x がマニュアルと異なっていたケース(補足ルール対象など)は除いてあり、それらは `SupplementaryRuleTests` でマニュアルの「バーコード・データ」を期待値にしている。出力が変わる修正をした場合は、マニュアルに照らして意図した変更か確認してから期待値を更新する。
- `cases.tsv` はタブ区切り(郵便番号・住所・シンボル列・画像ファイル名)。住所が空の行があるので、シェルで加工するときは連続するタブがまとめられないよう注意する。
- System.Drawing のテストは Windows 以外ではスキップされる。
- `TestData/ken_all_sample.csv` は utf_ken_all.csv の形式で手書きしたテスト用データ(実データの抜粋ではない)。実際の郵便番号データ(約12万件)はリポジトリに含めない。
- `TestData/jigyosyo_sample.csv` は JIGYOSYO.CSV の形式で手書きしたテスト用データで、実データと同じく Shift_JIS・CRLF。編集するときは UTF-8 で書いて `iconv -f UTF-8 -t CP932` で変換する(`.gitattributes` で binary 指定)。

## コード規約

Expand Down
65 changes: 57 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[郵便局 - 郵便番号・バーコードマニュアル](http://www.post.japanpost.jp/zipcode/zipmanual/index.html)

## 構成
すべて .NET Standard 2.0 のライブラリです。描画方法に合わせてパッケージを選んでください。
.NET Standard 2.0 のライブラリです(コアは .NET 8 以降向けのビルドも含み、住所の照合や郵便番号データの読み込みでメモリの割り当てが少なくなります)。描画方法に合わせてパッケージを選んでください。

| プロジェクト | 依存 | 用途 |
| --- | --- | --- |
Expand Down Expand Up @@ -53,13 +53,65 @@ CustomerBarCode code = BarCode.Create("1020082", "東京都千代田区一番町
```

- `ZipCodeDictionary` は破棄(`Dispose`)不要です。`Load(パス)` はファイルを読み終えると閉じます。`Load(Stream)` は渡された Stream を閉じないので、呼び出し側で破棄してください。
- 読み込んだデータはメモリ上に保持します(全国分で約 22MB)。
- 都道府県名の省略、町域名の前の「大字」「字」の有無、空白、全角・半角、「ヶ」と「ケ」の違いは区別しません。
- 読み込んだデータはメモリ上に保持します(全国分で約 24MB、事業所の個別郵便番号データも読み込むと約 44MB)。
- 都道府県名・郡名の省略、町域名の前の「大字」「字」の有無、空白、全角・半角、英字の大文字・小文字、「ヶ」と「ケ」、漢数字と算用数字(北六条 と 北6条)の違いは区別しません。
- 町域名が一致せず分割できない場合は、住所全体から抜き出します(郵便番号データなしと同じ)。分割できるかどうかは `dictionary.TrySplitAddress()` で確認できます。
- 代表の郵便番号(町域名が「以下に掲載がない場合」)は町域名がわからないため、分割しません。
- `IsRepresentativeZipCode` は郵便番号データに基づいて判定します。郵便番号データにない番号(郵便番号データより新しい番号など)は、下2けたで判定します。
- 従来の Shift_JIS 形式(KEN_ALL.CSV)は、長い町域名が複数行に分かれているため対象外です。
- 大口事業所の個別郵便番号は郵便番号データに含まれないため、分割できません。
### 事業所の個別郵便番号データを使う
日本郵便の[事業所の個別郵便番号データ](https://www.post.japanpost.jp/zipcode/dl/jigyosyo/index-zip.html)(`jigyosyo.zip`、Shift_JIS)も一緒に読み込むと、大口事業所・私書箱の個別郵便番号を扱えます。

```csharp
// Shift_JIS を使えるようにする(.NET Core / .NET 5 以降。.NET Framework では不要)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

ZipCodeDictionary dictionary = ZipCodeDictionary.Load(@"c:\data\utf_ken_all.zip", @"c:\data\jigyosyo.zip");

// 個別郵便番号のバーコードは、住所表示番号を CC4 で埋める
CustomerBarCode code = BarCode.Create("1008792", "東京都千代田区大手町2-3-1", dictionary);
// code.AddressNumber → ""
```

- [マニュアル p22](https://www.post.japanpost.jp/zipcode/zipmanual/p22.html) に従い、個別郵便番号あての郵便物は個別番号のみをバーコード化し、住所表示番号の 13 けたは CC4 で埋めます。事業所の個別郵便番号データを読み込んでいない場合は、住所から住所表示番号を抜き出してしまうので注意してください。
- `dictionary.Find(郵便番号)` で、事業所名(`BusinessName`)、番地等(`Block`)、種類(`Kind`:大口事業所・私書箱)を取得できます。
- `Encoding.RegisterProvider` を呼ばずに読み込むと `InvalidOperationException` になります。Shift_JIS を自分で用意する場合は `Load(Stream, Stream, Encoding)` を使ってください。

### 住所から郵便番号を探す
郵便番号データを使うと、住所から郵便番号を探せます。

```csharp
if (dictionary.TryGetZipCode("東京都千代田区一番町5-3", out var zipCode))
{
// zipCode → "1020082"
CustomerBarCode code = BarCode.Create(zipCode, "東京都千代田区一番町5-3", dictionary);
}

// 候補をすべて取得する(郵便番号が1つに決まらない場合は複数)
IReadOnlyList<ZipCodeEntry> candidates = dictionary.FindByAddress("北海道深川市音江町123");

// 事業所名も渡すと、大口事業所・私書箱の個別郵便番号を探せる(事業所の個別郵便番号データが必要)
dictionary.TryGetZipCode("東京都千代田区大手町2-3-1", "日本郵便株式会社", out var businessZipCode); // → "1008792"
```

- 都道府県名・市区町村名・町域名の順に照合し、最も長く一致した町域を返します。表記の違いは住所の分割と同じく区別しません。
- 同じ町域名で郵便番号が複数ある場合は、郵便番号データの括弧書きで絞り込みます。
- 超高層ビル(「霞が関ビル(10階)」など):住所の町域名より後ろにビル名があれば、その後の階(「10階」「10F」)で選びます。階がない場合、地下の場合、データにない階の場合は「(地階・階層不明)」、ビル名がなければ「(次のビルを除く)」などを使います。
- 地区名(「音江町(稲田)」「音江町(国見23〜298番地)」など):町域名の直後(前に「字」があってもよい)の地区名で選びます。
- 丁目・番地の範囲(「北六条東(1〜7丁目)」「常盤(1〜131番地)」など):町域名(または地区名)の直後の丁目・番地で選びます。範囲に当てはまらない場合は「(その他)」を使います。
- 同名の市区町村(府中市など)は、都道府県名がなければ町域名まで一致した方を使います。
- 町域名が一致しない場合は、マニュアルに従って代表の郵便番号(「以下に掲載がない場合」)を返します(`ZipCodeEntry.IsRepresentative` が `true`)。代表の郵便番号の郵便物にはカスタマバーコードを付けません。
- 事業所名を渡した場合は、住所の市区町村にある事業所から事業所名が一致するもの(空白、全角・半角、英字の大文字・小文字、「株式会社」「(株)」などの違いは区別しない)を探します。一致しなければ、一方が他方を含む事業所のうち、住所が番地等まで一致するもの(支店名の省略など)を探します。同じ事業所名が複数あれば番地等まで一致するものを使い、見つからなければ住所だけで探します。
- `TryGetZipCode` は郵便番号が 1 つに決まった場合だけ `true` を返します。決まらない場合は `FindByAddress` で候補を確認してください。

次の住所は郵便番号を決められないか、正しく探せません。
- 京都市の通り名を含む住所(「烏丸通御池上る二条殿町」など)は、町域名の前に通り名があるため町域名が一致せず、代表の郵便番号になります。
- 地区名が書かれていない住所や、郵便番号データの括弧書きを解釈できない町域は、候補が複数になります。
- 1 つの事業所が複数の個別郵便番号を持つ場合は、候補が複数になります。

全国の郵便番号データ(2026年10月更新)で、データから組み立てた住所で試した結果です。実際の住所の書き方の揺れまでは確かめていません。
- 町域(代表の郵便番号以外の 122,657 町域、「都道府県 + 市区町村 + 町域 + 番地」。超高層ビルは「…番地 + ビル名 + 階」、地区名は「町域 + 地区名 + 番地」):99.2% で正しい郵便番号に 1 つに決まり、0.7% は候補が複数で正しい郵便番号を含んでいました(残り 0.04% は、組み立てた住所の番地が同じ町域の別の郵便番号の範囲に入ったものなどで、その郵便番号が返ります)。
- 事業所(22,420 件、「データの住所 + 事業所名」):99.6% で正しい個別郵便番号に 1 つに決まり、残りは候補が複数で正しい個別郵便番号を含んでいました(ほとんどは複数の個別郵便番号を持つ事業所)。

### バーコードの内容
```csharp
Expand Down Expand Up @@ -132,7 +184,4 @@ foreach (var bar in code.GetBars())
| 1.x | 2.x |
| --- | --- |
| `BarCode.CreateImage(zip, address)` | `BarCode.Create(zip, address).ToBitmap()`(Drawing) |
| `BarCode.Export(zip, address, path)` | `BarCode.Create(zip, address).Save(path, ImageFormat.Gif)`(Drawing) |

## 備考
* 住所→郵便番号変換辞書検討中(郵便番号の入力が不要に)
| `BarCode.Export(zip, address, path)` | `BarCode.Create(zip, address).Save(path, ImageFormat.Gif)`(Drawing) |
Loading