From a9b86c0bd7840bd286f20755e432b5cfc8392ca1 Mon Sep 17 00:00:00 2001 From: cssho Date: Thu, 5 Nov 2026 21:14:53 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BD=8F=E6=89=80=E8=A1=A8=E7=A4=BA=E7=95=AA?= =?UTF-8?q?=E5=8F=B7=E3=81=AE13=E3=81=91=E3=81=9F=E7=9B=AE=E3=81=AB?= =?UTF-8?q?=E3=82=A2=E3=83=AB=E3=83=95=E3=82=A1=E3=83=99=E3=83=83=E3=83=88?= =?UTF-8?q?=E3=81=8C=E6=9D=A5=E3=82=8B=E3=81=A8=E8=90=BD=E3=81=A1=E3=82=8B?= =?UTF-8?q?=E4=B8=8D=E5=85=B7=E5=90=88=E3=81=AA=E3=81=A9=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 住所表示番号を13けたで切り捨て、13けた目がアルファベットの場合は 英字用制御コードのみ残すようにした(マニュアル p20) - 郵便番号の検証で末尾の改行や ASCII 以外の数字を受け付けないようにした - 住所の大文字変換を実行環境のカルチャに依存しないようにした - 全角ハイフン(U+FF0D)を半角に変換するようにした - 描画中に例外が出た場合に生成済みのイメージを破棄するようにした - Symbols を外部から書き換えられないようにした Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HNb8H3ydQTR8dWU8d28osE --- .../CustomerBarCodeDrawingExtensions.cs | 19 +++++-- .../CustomerBarCodeSkiaExtensions.cs | 21 +++++--- .../BarCodeTests.cs | 54 ++++++++++++++++++- ZipConvertCustomerBarCode/BarCode.cs | 39 ++++++-------- ZipConvertCustomerBarCode/CustomerBarCode.cs | 6 ++- 5 files changed, 101 insertions(+), 38 deletions(-) diff --git a/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs b/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs index 91d7363..e7ef546 100644 --- a/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs +++ b/ZipConvertCustomerBarCode.Drawing/CustomerBarCodeDrawingExtensions.cs @@ -20,15 +20,24 @@ public static Bitmap ToBitmap(this CustomerBarCode code, int scale = 1) if (code == null) throw new ArgumentNullException(nameof(code)); var img = new Bitmap(code.GetWidth(scale), code.GetHeight(scale)); - using (var gr = Graphics.FromImage(img)) + try { - gr.Clear(Color.White); - foreach (var bar in code.GetBars(scale)) + using (var gr = Graphics.FromImage(img)) { - gr.FillRectangle(Brushes.Black, bar.X, bar.Y, bar.Width, bar.Height); + gr.Clear(Color.White); + foreach (var bar in code.GetBars(scale)) + { + gr.FillRectangle(Brushes.Black, bar.X, bar.Y, bar.Width, bar.Height); + } } + return img; + } + catch + { + // 描画に失敗した場合は生成済みのイメージを破棄する + img.Dispose(); + throw; } - return img; } /// diff --git a/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs b/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs index 0dc7371..19a977b 100644 --- a/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs +++ b/ZipConvertCustomerBarCode.SkiaSharp/CustomerBarCodeSkiaExtensions.cs @@ -20,16 +20,25 @@ public static SKBitmap ToSKBitmap(this CustomerBarCode code, int scale = 1) if (code == null) throw new ArgumentNullException(nameof(code)); var img = new SKBitmap(code.GetWidth(scale), code.GetHeight(scale)); - using (var canvas = new SKCanvas(img)) - using (var paint = new SKPaint { Color = SKColors.Black, IsAntialias = false }) + try { - canvas.Clear(SKColors.White); - foreach (var bar in code.GetBars(scale)) + using (var canvas = new SKCanvas(img)) + using (var paint = new SKPaint { Color = SKColors.Black, IsAntialias = false }) { - canvas.DrawRect(SKRect.Create(bar.X, bar.Y, bar.Width, bar.Height), paint); + canvas.Clear(SKColors.White); + foreach (var bar in code.GetBars(scale)) + { + canvas.DrawRect(SKRect.Create(bar.X, bar.Y, bar.Width, bar.Height), paint); + } } + return img; + } + catch + { + // 描画に失敗した場合は生成済みのイメージを破棄する + img.Dispose(); + throw; } - return img; } /// diff --git a/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs b/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs index 8c456b8..e7bc4b9 100644 --- a/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs +++ b/ZipConvertCustomerBarCode.Tests/BarCodeTests.cs @@ -1,4 +1,6 @@ -namespace ZipConvertCustomerBarCode.Tests +using System.Globalization; + +namespace ZipConvertCustomerBarCode.Tests { public class BarCodeTests { @@ -11,11 +13,29 @@ public void シンボル列が1x版と一致する(string zipCode, string addres Assert.Equal(expected.Split(' '), code.Symbols); } + // 住所表示番号は13けたまで。13けた目がアルファベットの場合は英字用制御コードのみ残す + // http://www.post.japanpost.jp/zipcode/zipmanual/p20.html + [Theory] + // マニュアル p20 の例 + [InlineData("9100067", "福井県福井市新田塚3丁目80-25 J1ビル2-B", "STC 9 1 0 0 0 6 7 3 - 8 0 - 2 5 CC1 9 1 - 2 CC1 9 SPC")] + // アルファベットで13けたを超える + [InlineData("1234567", "A1B2C3D4E5F6G7", "STC 1 2 3 4 5 6 7 CC1 0 1 CC1 1 2 CC1 2 3 CC1 3 4 CC1 CC5 SPC")] + [InlineData("1234567", "123456789012A", "STC 1 2 3 4 5 6 7 1 2 3 4 5 6 7 8 9 0 1 2 CC1 8 SPC")] + // アルファベットが12~13けた目にちょうど収まる + [InlineData("1234567", "12345678901A", "STC 1 2 3 4 5 6 7 1 2 3 4 5 6 7 8 9 0 1 CC1 0 - SPC")] + public void 住所表示番号は13けたで切り捨てる(string zipCode, string address, string expected) + { + var code = BarCode.Create(zipCode, address); + + Assert.Equal(expected.Split(' '), code.Symbols); + } + [Theory] [InlineData("0140113")] [InlineData("014-0113")] [InlineData("0140113")] [InlineData("014‐0113")] + [InlineData("014-0113")] public void 郵便番号は全角とハイフンを受け付ける(string zipCode) { var code = BarCode.Create(zipCode, "添60-1"); @@ -29,11 +49,43 @@ public void 郵便番号は全角とハイフンを受け付ける(string zipCod [InlineData("01401130")] [InlineData("014011A")] [InlineData("014 0113")] + [InlineData("0140113\n")] + // アラビア・インド数字 + [InlineData("٠١٤٠١١٣")] public void 郵便番号が7桁の数値でなければ例外(string zipCode) { Assert.Throws(() => BarCode.Create(zipCode, "添60-1")); } + [Fact] + public void 住所の大文字変換は実行環境のカルチャによらない() + { + var original = CultureInfo.CurrentCulture; + try + { + // tr-TR では "i".ToUpper() が "İ" になる + CultureInfo.CurrentCulture = new CultureInfo("tr-TR"); + + var code = BarCode.Create("1234567", "1-2i"); + + Assert.Equal(new[] { "1", "-", "2", "CC1", "8" }, code.Symbols.Skip(8).Take(5)); + } + finally + { + CultureInfo.CurrentCulture = original; + } + } + + [Fact] + public void シンボル列は書き換えられない() + { + var code = BarCode.Create("0140113", "添60-1"); + + Assert.False(code.Symbols is string[]); + var list = Assert.IsAssignableFrom>(code.Symbols); + Assert.Throws(() => list[1] = "X"); + } + [Fact] public void 引数がnullなら例外() { diff --git a/ZipConvertCustomerBarCode/BarCode.cs b/ZipConvertCustomerBarCode/BarCode.cs index 226db0c..4c64ea7 100644 --- a/ZipConvertCustomerBarCode/BarCode.cs +++ b/ZipConvertCustomerBarCode/BarCode.cs @@ -32,7 +32,7 @@ public static class BarCode {'p','p'},{'q','q'},{'r','r'},{'s','s'},{'t','t'}, {'u','u'},{'v','v'},{'w','w'},{'x','x'},{'y','y'}, {'z','z'}, - {' ',' '},{'‐','-'} + {' ',' '},{'‐','-'},{'-','-'} }; // 文字→バーコード辞書 @@ -75,7 +75,7 @@ public static CustomerBarCode Create(string zipCode, string address) if (address == null) throw new ArgumentNullException(nameof(address)); var code = ConvertCustomerBarCode(address, CheckPostCode(zipCode)); - return new CustomerBarCode(code.ToArray()); + return new CustomerBarCode(code); } /// @@ -92,7 +92,8 @@ private static IEnumerable CheckPostCode(string zipCode) // ハイフン除去 zipCode = zipCode.Replace("-", ""); - if (!Regex.IsMatch(zipCode, @"\A\d{7}\Z")) + // \d は全角以外の Unicode 数字にも、\Z は末尾の改行にもマッチするため使わない + if (!Regex.IsMatch(zipCode, @"\A[0-9]{7}\z")) { throw new ArgumentException("郵便番号は7桁の数値で入力してください。"); } @@ -118,7 +119,8 @@ private static IEnumerable ConvertCustomerBarCode(string address, IEnume // http://www.post.japanpost.jp/zipcode/zipmanual/p19.html // 1. まず、データ内にあるアルファベットの小文字は大文字に置き換えます。 - address = address.ToUpper(); + // 実行環境のカルチャによらず変換する(tr-TR では i が İ になるため) + address = address.ToUpperInvariant(); // 2. 同様に、データ内にある"&"等の下記の文字は取り除き、後ろのデータを詰めます。 //「&」(アンパサンド)、「/」(スラッシュ)、「・」(中グロ)、「.」(ピリオド) @@ -173,26 +175,15 @@ private static string TrimHyphen(string target) /// カスタマバーコード private static string[] ConvertCCode(string address) { - int idx = 0; - var customerCode = Enumerable.Repeat("CC4", 13).ToArray(); - foreach (var c in address.Select(x => x.ToString()).Take(13)) - { - var cc = BarcodeCharDic[c]; - if (cc.Length == 1) - { - // 数値、ハイフン - customerCode[idx] = cc[0]; - } - else - { - // アルファベット - customerCode[idx] = cc[0]; - customerCode[idx + 1] = cc[1]; - idx++; - } - idx++; - } - return customerCode; + // 数値、ハイフンは1けた、アルファベットは英字用制御コードと数字の2けたに変換し、 + // 13けたに満たない場合は CC4 で埋める + // http://www.post.japanpost.jp/zipcode/zipmanual/p20.html + // 合計けた数が20けた(住所表示番号は13けた)を超えた場合、以降の文字については切り捨てます。 + // その際、20~21けた目がアルファベット文字となった場合、20けた目の英字用制御(CC1、CC2、CC3)は残して以降は切り捨てます。 + return address.SelectMany(c => BarcodeCharDic[c.ToString()]) + .Concat(Enumerable.Repeat("CC4", 13)) + .Take(13) + .ToArray(); } /// diff --git a/ZipConvertCustomerBarCode/CustomerBarCode.cs b/ZipConvertCustomerBarCode/CustomerBarCode.cs index c1c0bef..7d098e8 100644 --- a/ZipConvertCustomerBarCode/CustomerBarCode.cs +++ b/ZipConvertCustomerBarCode/CustomerBarCode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Text; @@ -31,9 +32,10 @@ public sealed class CustomerBarCode {"STC"," LD"},{"SPC","DL "} }; - internal CustomerBarCode(IReadOnlyList symbols) + internal CustomerBarCode(IEnumerable symbols) { - Symbols = symbols; + // 呼び出し側から書き換えられないよう、コピーを読み取り専用で保持する + Symbols = new ReadOnlyCollection(symbols.ToArray()); } ///