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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- `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万件)はリポジトリに含めない。

## コード規約

Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ CustomerBarCode code = BarCode.Create("1000013", "東京都千代田区霞が関

ただしこの場合は住所全体を住所Bとして扱うため、町域名に「丁目・番・ノ」などの前の漢数字を含む住所(一番町、八丁堀、三ノ輪 など)では、町域名の漢数字も抜き出されて誤ったバーコードになります。住所A・住所Bに分けて渡すことをおすすめします。

### 郵便番号データを使う
日本郵便の[郵便番号データ(1レコード1行、UTF-8形式)](https://www.post.japanpost.jp/zipcode/dl/utf-zip.html)を読み込むと、郵便番号の町域名で住所を住所A・住所Bに自動で分割できます。住所全体を渡しても、町域名の漢数字(一番町、八丁堀など)を抜き出さなくなります。

データは同梱していないので、`utf_ken_all.zip` をダウンロードして指定してください(zip のままでも、展開した `utf_ken_all.csv` でも読み込めます)。データは毎月更新されます。

```csharp
// 読み込みには時間がかかるので、一度読み込んだものを使い回す
ZipCodeDictionary dictionary = ZipCodeDictionary.Load(@"c:\data\utf_ken_all.zip");

CustomerBarCode code = BarCode.Create("1020082", "東京都千代田区一番町5-3", dictionary);
// code.AddressNumber → "5-3"(郵便番号データなしでは "1-5-3")
```

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

### バーコードの内容
```csharp
string zip = code.ZipCode; // "1000013"
Expand All @@ -48,7 +70,7 @@ bool rep = code.IsRepresentativeZipCode; // 代表の郵便番号か

`AddressNumber` は 13 けたで切り捨てる前の値です。バーコードに含まれるのは先頭 13 けた分です。

代表の郵便番号(郵便番号簿の「以下に掲載がない場合」の番号)の郵便物には、カスタマバーコードを付けないことになっています([マニュアル p18](https://www.post.japanpost.jp/zipcode/zipmanual/p18.html))。`IsRepresentativeZipCode` は、マニュアルの「原則として下2けたが 00」に従って判定します。
代表の郵便番号(郵便番号簿の「以下に掲載がない場合」の番号)の郵便物には、カスタマバーコードを付けないことになっています([マニュアル p18](https://www.post.japanpost.jp/zipcode/zipmanual/p18.html))。`IsRepresentativeZipCode` は、郵便番号データを使わない場合はマニュアルの「原則として下2けたが 00」に従って判定する目安です。正確に判定するには郵便番号データを使ってください。

### 印刷用の実寸
[マニュアル p12](https://www.post.japanpost.jp/zipcode/zipmanual/p12.html) の寸法(10 ポイントのときバー幅 0.6mm、バーピッチ 1.2mm、ロングバー 3.6mm)で出力します。大きさは 8〜11.5 ポイントで指定でき、省略時は 10 ポイントです。
Expand Down
17 changes: 12 additions & 5 deletions ZipConvertCustomerBarCode.Tests/SupplementaryRuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,19 @@ public void 住所Bだけから抜き出す()
Assert.Equal("1-2", ToBarCodeData(BarCode.Create("1100011", "東京都台東区三ノ輪", "1丁目2")));
}

[Fact]
public void 住所Bが住所Aで始まる場合は住所Aを取り除く()
[Theory]
[InlineData("1040032", "東京都中央区八丁堀", "東京都中央区八丁堀2丁目1-1", "2-1-1")]
// 空白、全角・半角、ヶ と ケ の違いは区別しない
[InlineData("1020082", "東京都千代田区一番町", "東京都 千代田区一番町5-3", "5-3")]
[InlineData("1020082", " 東京都千代田区一番町 ", "東京都千代田区一番町5-3", "5-3")]
[InlineData("2730102", "千葉県鎌ケ谷市右京塚", "千葉県鎌ヶ谷市右京塚東3丁目-20-5", "3-20-5")]
// 住所Aが空
[InlineData("1040032", "", "2丁目1-1", "2-1-1")]
public void 住所Bが住所Aで始まる場合は住所Aを取り除く(string zipCode, string townAddress, string blockAddress, string expected)
{
var code = BarCode.Create("1040032", "東京都中央区八丁堀", "東京都中央区八丁堀2丁目1-1");
var code = BarCode.Create(zipCode, townAddress, blockAddress);

Assert.Equal("2-1-1", ToBarCodeData(code));
Assert.Equal(expected, ToBarCodeData(code));
}

[Fact]
Expand All @@ -97,7 +104,7 @@ public void 住所全体を渡すと町域名の漢数字も抜き出される()
public void 住所Aと住所Bがnullなら例外()
{
Assert.Throws<ArgumentNullException>(() => BarCode.Create("1020082", null!, "5-3"));
Assert.Throws<ArgumentNullException>(() => BarCode.Create("1020082", "東京都千代田区一番町", null!));
Assert.Throws<ArgumentNullException>(() => BarCode.Create("1020082", "東京都千代田区一番町", (string)null!));
}

// 住所表示番号のシンボルをバーコード・データの文字列に戻す(CC4 の埋め草は除く)
Expand Down
16 changes: 16 additions & 0 deletions ZipConvertCustomerBarCode.Tests/TestData/ken_all_sample.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
13101,"100 ","1000000","トウキョウト","チヨダク","イカニケイサイガナイバアイ","東京都","千代田区","以下に掲載がない場合",0,0,0,0,0,0
13101,"100 ","1000013","トウキョウト","チヨダク","カスミガセキ","東京都","千代田区","霞が関",0,0,1,0,0,0
13101,"102 ","1020082","トウキョウト","チヨダク","イチバンチョウ","東京都","千代田区","一番町",0,0,0,0,0,0
13102,"104 ","1040032","トウキョウト","チュウオウク","ハッチョウボリ","東京都","中央区","八丁堀",0,0,1,0,0,0
13106,"110 ","1100011","トウキョウト","タイトウク","ミノワ","東京都","台東区","三ノ輪",0,0,1,0,0,0
01103,"060 ","0600906","ホッカイドウ","サッポロシヒガシク","キタ6ジョウヒガシ(1-7チョウメ)","北海道","札幌市東区","北六条東(1~7丁目)",1,0,1,0,0,0
01103,"065 ","0650006","ホッカイドウ","サッポロシヒガシク","キタ6ジョウヒガシ(8-20チョウメ)","北海道","札幌市東区","北六条東(8~20丁目)",1,0,1,0,0,0
12103,"263 ","2630023","チバケン","チバシイナゲク","ミドリチョウ","千葉県","千葉市稲毛区","緑町",0,0,1,0,0,0
12224,"273 ","2730102","チバケン","カマガヤシ","ウキョウヅカ","千葉県","鎌ケ谷市","右京塚",0,0,0,0,0,0
47356,"90136","9013601","オキナワケン","シマジリグントナキソン","トナキソンイチエン","沖縄県","島尻郡渡名喜村","渡名喜村一円",0,0,0,0,0,0
08546,"30604","3060433","イバラキケン","サシマグンサカイマチ","サカイマチノツギニバンチガクルバアイ","茨城県","猿島郡境町","境町の次に番地がくる場合",0,0,0,0,0,0
25443,"52223","5220341","シガケン","イヌカミグンタガチョウ","イチエン","滋賀県","犬上郡多賀町","一円",0,0,0,0,0,0
99999,"999 ","9999901","テストケン","テストシ","ホンチョウ","テスト県","テスト市","本町",0,0,0,1,0,0
99999,"999 ","9999901","テストケン","テストシ","ホンチョウキタ","テスト県","テスト市","本町北",0,0,0,1,0,0
99999,"999 ","9999902","テストケン","テストシ","ニシ2ジョウ","テスト県","テスト市","西2条",0,0,1,0,0,0
03202,"02702","0270203","イワテケン","ミヤコシ","ツガルイシ","岩手県","宮古市","津軽石",0,1,0,0,0,0
206 changes: 206 additions & 0 deletions ZipConvertCustomerBarCode.Tests/ZipCodeDictionaryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using System.IO.Compression;
using System.Text;

namespace ZipConvertCustomerBarCode.Tests
{
// TestData/ken_all_sample.csv は utf_ken_all.csv の形式で書いたテスト用のデータ
// (実在の住所をもとにしているが、内容は郵便番号データと一致しない場合がある。999で始まる番号は架空)
public class ZipCodeDictionaryTests
{
private static readonly ZipCodeDictionary Dictionary = ZipCodeDictionary.Load(TestCases.DataPath("ken_all_sample.csv"));

[Fact]
public void CSVファイルを読み込める()
{
Assert.Equal(15, Dictionary.Count);
}

[Fact]
public void zipファイルのまま読み込める()
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".zip");
try
{
using (var archive = ZipFile.Open(path, ZipArchiveMode.Create))
{
archive.CreateEntryFromFile(TestCases.DataPath("ken_all_sample.csv"), "utf_ken_all.csv");
}

Assert.Equal(15, ZipCodeDictionary.Load(path).Count);
}
finally
{
File.Delete(path);
}
}

[Fact]
public void Streamから読み込める()
{
using var stream = File.OpenRead(TestCases.DataPath("ken_all_sample.csv"));

Assert.Equal(15, ZipCodeDictionary.Load(stream).Count);
// 渡された Stream は閉じない
Assert.True(stream.CanRead);
}

[Fact]
public void 列数が違えば例外()
{
var csv = "13101,\"100 \",\"1000013\",\"東京都\",\"千代田区\",\"霞が関\"\r\n";

var e = Assert.Throws<InvalidDataException>(() => Load(Encoding.UTF8.GetBytes(csv)));
Assert.Contains("1行目", e.Message);
}

[Fact]
public void UTF8でなければ例外()
{
// Shift_JIS の「東京都」
var sjis = new byte[] { 0x93, 0x8C, 0x8B, 0x9E, 0x93, 0x73 };
var line = Encoding.ASCII.GetBytes("13101,\"100 \",\"1000013\",\"x\",\"x\",\"x\",\"")
.Concat(sjis)
.Concat(Encoding.ASCII.GetBytes("\",\"x\",\"x\",0,0,1,0,0,0\r\n"))
.ToArray();

Assert.Throws<InvalidDataException>(() => Load(line));
}

[Fact]
public void 郵便番号が7桁の数値でなければ例外()
{
var csv = "13101,\"100 \",\"100001\",\"x\",\"x\",\"x\",\"東京都\",\"千代田区\",\"霞が関\",0,0,1,0,0,0\r\n";

Assert.Throws<InvalidDataException>(() => Load(Encoding.UTF8.GetBytes(csv)));
}

[Theory]
[InlineData("1000000", true)]
[InlineData("100-0013", false)]
// 郵便番号データにない(事業所の個別番号など)
[InlineData("1008798", null)]
public void 代表の郵便番号を判定できる(string zipCode, bool? expected)
{
Assert.Equal(expected, Dictionary.IsRepresentative(zipCode));
}

[Fact]
public void 郵便番号の町域を取得できる()
{
Assert.True(Dictionary.Contains("1000-013"));
Assert.False(Dictionary.Contains("1008798"));

var entry = Assert.Single(Dictionary.Find("1000013"));
Assert.Equal(("1000013", "東京都", "千代田区", "霞が関"), (entry.ZipCode, entry.Prefecture, entry.City, entry.Town));
Assert.Equal(new[] { "本町", "本町北" }, Dictionary.Find("9999901").Select(x => x.Town));
Assert.Empty(Dictionary.Find("1008798"));
}

[Theory]
[InlineData("1020082", "東京都千代田区一番町5-3", "東京都千代田区一番町", "5-3")]
// 都道府県名の省略
[InlineData("1020082", "千代田区一番町5-3", "千代田区一番町", "5-3")]
// 空白
[InlineData("1020082", "東京都 千代田区 一番町 5-3", "東京都 千代田区 一番町", " 5-3")]
// ヶ と ケ
[InlineData("2730102", "千葉県鎌ヶ谷市右京塚 東3丁目-20-5", "千葉県鎌ヶ谷市右京塚", " 東3丁目-20-5")]
// 全角・半角
[InlineData("9999902", "テスト県テスト市西2条3丁目", "テスト県テスト市西2条", "3丁目")]
// 町域名の括弧書き
[InlineData("0600906", "北海道札幌市東区北六条東4丁目 郵便センター6号館", "北海道札幌市東区北六条東", "4丁目 郵便センター6号館")]
// 町域名の前の「大字」「字」
[InlineData("0270203", "岩手県宮古市大字津軽石第二地割原 郵便番号CD-1", "岩手県宮古市大字津軽石", "第二地割原 郵便番号CD-1")]
[InlineData("0270203", "宮古市字津軽石10", "宮古市字津軽石", "10")]
[InlineData("0270203", "岩手県宮古市津軽石10", "岩手県宮古市津軽石", "10")]
// ○○村一円
[InlineData("9013601", "沖縄県島尻郡渡名喜村1917", "沖縄県島尻郡渡名喜村", "1917")]
// ○○町の次に番地がくる場合
[InlineData("3060433", "茨城県猿島郡境町123", "茨城県猿島郡境町", "123")]
// 町域名が「一円」
[InlineData("5220341", "滋賀県犬上郡多賀町一円12", "滋賀県犬上郡多賀町一円", "12")]
// 1つの郵便番号に複数の町域がある場合は長く一致した方
[InlineData("9999901", "テスト県テスト市本町北1-2", "テスト県テスト市本町北", "1-2")]
[InlineData("9999901", "テスト市本町1-2", "テスト市本町", "1-2")]
public void 住所を町域名で分割できる(string zipCode, string address, string town, string block)
{
Assert.True(Dictionary.TrySplitAddress(zipCode, address, out var townAddress, out var blockAddress));
Assert.Equal(town, townAddress);
Assert.Equal(block, blockAddress);
}

[Theory]
// 郵便番号データにない
[InlineData("1008798", "東京都千代田区丸の内2-7-2")]
// 町域名が一致しない
[InlineData("1020082", "大阪府大阪市北区梅田1-1")]
// 代表の郵便番号(以下に掲載がない場合)は町域名がわからない
[InlineData("1000000", "東京都千代田区一番町5-3")]
public void 分割できない場合はfalse(string zipCode, string address)
{
Assert.False(Dictionary.TrySplitAddress(zipCode, address, out var townAddress, out var blockAddress));
Assert.Null(townAddress);
Assert.Null(blockAddress);
}

[Theory]
[InlineData("1020082", "東京都千代田区一番町5-3", "5-3")]
[InlineData("1040032", "東京都中央区八丁堀2丁目1-1", "2-1-1")]
[InlineData("1100011", "台東区三ノ輪1丁目2", "1-2")]
public void 郵便番号データを使うと町域名の漢数字を抜き出さない(string zipCode, string address, string expected)
{
Assert.Equal(expected, BarCode.Create(zipCode, address, Dictionary).AddressNumber);
// 郵便番号データなしでは町域名の漢数字も抜き出される
Assert.NotEqual(expected, BarCode.Create(zipCode, address).AddressNumber);
}

[Fact]
public void 分割できない場合は住所全体から抜き出す()
{
var withDictionary = BarCode.Create("1020082", "大阪府大阪市北区梅田1-1", Dictionary);
var withoutDictionary = BarCode.Create("1020082", "大阪府大阪市北区梅田1-1");

Assert.Equal(withoutDictionary.Symbols, withDictionary.Symbols);
}

[Theory]
[InlineData("1000000", true)]
[InlineData("1000013", false)]
// 郵便番号データにない場合(郵便番号データが古い場合など)は下2けたで判定する
[InlineData("3510100", true)]
[InlineData("1008798", false)]
public void 郵便番号データで代表の郵便番号を判定する(string zipCode, bool expected)
{
Assert.Equal(expected, BarCode.Create(zipCode, "1-1", Dictionary).IsRepresentativeZipCode);
}

[Fact]
public void 代表の郵便番号では住所を分割しない()
{
var code = BarCode.Create("1000000", "東京都千代田区一番町5-3", Dictionary);

Assert.Equal(BarCode.Create("1000000", "東京都千代田区一番町5-3").Symbols, code.Symbols);
Assert.True(code.IsRepresentativeZipCode);
}

[Fact]
public void 郵便番号データなしでは下2けたで判定する()
{
Assert.True(BarCode.Create("3510100", "1-1").IsRepresentativeZipCode);
}

[Fact]
public void 引数がnullなら例外()
{
Assert.Throws<ArgumentNullException>(() => ZipCodeDictionary.Load((string)null!));
Assert.Throws<ArgumentNullException>(() => ZipCodeDictionary.Load((Stream)null!));
Assert.Throws<ArgumentNullException>(() => BarCode.Create("1020082", "一番町5-3", (ZipCodeDictionary)null!));
Assert.Throws<ArgumentNullException>(() => Dictionary.TrySplitAddress("1020082", null!, out _, out _));
}

private static ZipCodeDictionary Load(byte[] bytes)
{
using var stream = new MemoryStream(bytes);
return ZipCodeDictionary.Load(stream);
}
}
}
Loading