From 7cb27689796a61e69791085deb1af5da5a418105 Mon Sep 17 00:00:00 2001 From: cssho Date: Fri, 6 Nov 2026 00:20:55 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=B3=E3=82=A2=E3=82=92=20netstandard2.0=20?= =?UTF-8?q?=E3=81=A8=20net8.0=20=E3=81=AE=E3=83=9E=E3=83=AB=E3=83=81?= =?UTF-8?q?=E3=82=BF=E3=83=BC=E3=82=B2=E3=83=83=E3=83=88=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=80=81net8.0=20=E3=81=A7=E3=81=AF=20Span=20=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - net8.0 では、住所照合の正規化をスタック上で行い、CSV の読み込みで使う列だけを文字列にする - 住所の分割を都道府県名・市区町村名・町域名の順の照合に変え、候補文字列を作らないようにした(両方のビルド) - 郵便番号のチェックを LINQ・正規表現を使わないループにした(両方のビルド) - テストでコアのビルドを切り替えられるようにした(dotnet test -p:CoreTargetFramework=netstandard2.0) - README と CLAUDE.md を更新 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HNb8H3ydQTR8dWU8d28osE --- CLAUDE.md | 9 +- README.md | 2 +- .../TargetFrameworkTests.cs | 25 ++++ .../ZipConvertCustomerBarCode.Tests.csproj | 15 +- ZipConvertCustomerBarCode/AddressMatcher.cs | 98 +++++++++++-- ZipConvertCustomerBarCode/BarCode.cs | 95 ++++++++++-- .../ZipCodeDictionary.cs | 135 ++++++++++++++---- ZipConvertCustomerBarCode/ZipCodeEntry.cs | 42 +++--- .../ZipConvertCustomerBarCode.csproj | 3 +- 9 files changed, 345 insertions(+), 79 deletions(-) create mode 100644 ZipConvertCustomerBarCode.Tests/TargetFrameworkTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index 57008d8..c5b67b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,17 +8,20 @@ ## 構成・ビルド -- ライブラリは 3 つ、すべて `netstandard2.0`(SDK スタイル)。ビルドは `dotnet build ZipConvertCustomerBarCode.sln`。 - - `ZipConvertCustomerBarCode`: コア。外部依存を追加しない(描画ライブラリに依存させないのが分割の目的)。 +- ライブラリは 3 つ(SDK スタイル)。ビルドは `dotnet build ZipConvertCustomerBarCode.sln`。 + - `ZipConvertCustomerBarCode`: コア。`netstandard2.0` と `net8.0`。外部依存を追加しない(描画ライブラリに依存させないのが分割の目的。`System.Memory` も入れない)。 + - `Span` などは `#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 以外ではスキップされる。 diff --git a/README.md b/README.md index d515dc0..b476ccf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [郵便局 - 郵便番号・バーコードマニュアル](http://www.post.japanpost.jp/zipcode/zipmanual/index.html) ## 構成 -すべて .NET Standard 2.0 のライブラリです。描画方法に合わせてパッケージを選んでください。 +.NET Standard 2.0 のライブラリです(コアは .NET 8 以降向けのビルドも含み、住所の照合や郵便番号データの読み込みでメモリの割り当てが少なくなります)。描画方法に合わせてパッケージを選んでください。 | プロジェクト | 依存 | 用途 | | --- | --- | --- | diff --git a/ZipConvertCustomerBarCode.Tests/TargetFrameworkTests.cs b/ZipConvertCustomerBarCode.Tests/TargetFrameworkTests.cs new file mode 100644 index 0000000..67e6895 --- /dev/null +++ b/ZipConvertCustomerBarCode.Tests/TargetFrameworkTests.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.Versioning; + +namespace ZipConvertCustomerBarCode.Tests +{ + public class TargetFrameworkTests + { + [Fact] + public void 指定したコアのビルドでテストしている() + { + // dotnet test -p:CoreTargetFramework=netstandard2.0 で指定したビルドが実際に使われているか + var expected = typeof(TargetFrameworkTests).Assembly.GetCustomAttributes() + .Single(x => x.Key == "CoreTargetFramework").Value; + var actual = typeof(BarCode).Assembly.GetCustomAttribute()!.FrameworkName; + + var expectedName = expected switch + { + "netstandard2.0" => ".NETStandard,Version=v2.0", + "net8.0" => ".NETCoreApp,Version=v8.0", + _ => throw new InvalidOperationException($"想定していない CoreTargetFramework: {expected}"), + }; + Assert.Equal(expectedName, actual); + } + } +} diff --git a/ZipConvertCustomerBarCode.Tests/ZipConvertCustomerBarCode.Tests.csproj b/ZipConvertCustomerBarCode.Tests/ZipConvertCustomerBarCode.Tests.csproj index ab89d7e..bc1b47e 100644 --- a/ZipConvertCustomerBarCode.Tests/ZipConvertCustomerBarCode.Tests.csproj +++ b/ZipConvertCustomerBarCode.Tests/ZipConvertCustomerBarCode.Tests.csproj @@ -13,8 +13,21 @@ + + + net8.0 + + + + + + + - + diff --git a/ZipConvertCustomerBarCode/AddressMatcher.cs b/ZipConvertCustomerBarCode/AddressMatcher.cs index fac43bc..0744507 100644 --- a/ZipConvertCustomerBarCode/AddressMatcher.cs +++ b/ZipConvertCustomerBarCode/AddressMatcher.cs @@ -53,7 +53,14 @@ public int MatchPrefix(string prefix) /// 一致した部分の終わり(元の住所での位置)。一致しない場合は -1 public int MatchPrefix(string prefix, int start) { +#if NET8_0_OR_GREATER + // 正規化した文字列を作らず、スタック上で正規化して比べる(漢数字は1文字が最大4文字になる) + var size = prefix.Length * 4; + Span buffer = size <= 256 ? stackalloc char[size] : new char[size]; + return MatchNormalizedPrefix(buffer.Slice(0, NormalizeTo(prefix, buffer)), start); +#else return MatchNormalizedPrefix(Normalize(prefix), start); +#endif } /// @@ -64,12 +71,42 @@ public int MatchPrefix(string prefix, int start) /// 一致した部分の終わり(元の住所での位置)。一致しない場合は -1 public int MatchNormalizedPrefix(string target, int start) { +#if NET8_0_OR_GREATER + return MatchNormalizedPrefix(target.AsSpan(), start); +#else var offset = NormalizedIndex(start); if (target.Length == 0) return start; - if (string.CompareOrdinal(normalized, offset, target, 0, target.Length) != 0 || normalized.Length - offset < target.Length) return -1; + if (normalized.Length - offset < target.Length || string.CompareOrdinal(normalized, offset, target, 0, target.Length) != 0) return -1; + return MatchEnd(offset, target.Length); +#endif + } + +#if NET8_0_OR_GREATER + /// + /// 住所の指定位置以降が、正規化済みの町域名などと一致する場合、一致した部分の終わりの位置を返す + /// + /// 正規化済みの町域名など + /// 照合を始める位置(元の住所での位置) + /// 一致した部分の終わり(元の住所での位置)。一致しない場合は -1 + public int MatchNormalizedPrefix(ReadOnlySpan target, int start) + { + var offset = NormalizedIndex(start); + if (target.Length == 0) return start; + if (!normalized.AsSpan(offset).StartsWith(target, StringComparison.Ordinal)) return -1; + return MatchEnd(offset, target.Length); + } +#endif + /// + /// 正規化後の住所で一致した部分の終わりを、元の住所での位置に変換する + /// + /// 一致した部分の開始位置(正規化後) + /// 一致した部分の長さ(正規化後) + /// 一致した部分の終わり(元の住所での位置)。漢数字の途中で終わる場合は -1 + private int MatchEnd(int offset, int length) + { // 漢数字の途中で終わる場合(「二十」の「2」だけが一致した場合など)は一致としない - var last = offset + target.Length - 1; + var last = offset + length - 1; if (last + 1 < normalized.Length && ends[last + 1] == ends[last]) return -1; return ends[last]; } @@ -149,16 +186,15 @@ private static void Normalize(string value, StringBuilder sb, List ends) } // 漢数字の並びは算用数字に変換する - if (KanjiNumerals.IndexOf(c) >= 0) + var kanjiEnd = KanjiRunEnd(value, i); + if (kanjiEnd > i) { - int j = i; - while (j < value.Length && KanjiNumerals.IndexOf(value[j]) >= 0) j++; - foreach (var d in BarCode.KanjiToNumber(value.Substring(i, j - i))) + foreach (var d in BarCode.KanjiToNumber(value, i, kanjiEnd - i)) { sb.Append(d); - ends?.Add(j); + ends?.Add(kanjiEnd); } - i = j; + i = kanjiEnd; continue; } @@ -168,6 +204,52 @@ private static void Normalize(string value, StringBuilder sb, List ends) } } +#if NET8_0_OR_GREATER + /// + /// 住所を照合用に正規化して書き込む( の文字列を作らない版) + /// + /// 住所 + /// 書き込み先(住所の長さの4倍あれば足りる) + /// 書き込んだ文字数 + private static int NormalizeTo(string value, Span destination) + { + int i = 0, written = 0; + while (i < value.Length) + { + var c = value[i]; + if (char.IsWhiteSpace(c)) + { + i++; + continue; + } + + // 漢数字の並びは算用数字に変換する + var kanjiEnd = KanjiRunEnd(value, i); + if (kanjiEnd > i) + { + written += BarCode.WriteKanjiNumber(value, i, kanjiEnd - i, destination.Slice(written)); + i = kanjiEnd; + continue; + } + + destination[written++] = NormalizeChar(c); + i++; + } + return written; + } +#endif + + /// + /// 指定位置から漢数字が続く場合、その終わりの位置を返す + /// + /// 漢数字の並びの終わりの位置。漢数字で始まらない場合は指定位置 + private static int KanjiRunEnd(string value, int start) + { + int end = start; + while (end < value.Length && KanjiNumerals.IndexOf(value[end]) >= 0) end++; + return end; + } + /// /// 元の住所での位置を正規化後の位置に変換する /// diff --git a/ZipConvertCustomerBarCode/BarCode.cs b/ZipConvertCustomerBarCode/BarCode.cs index c485ad3..a738ca7 100644 --- a/ZipConvertCustomerBarCode/BarCode.cs +++ b/ZipConvertCustomerBarCode/BarCode.cs @@ -202,19 +202,24 @@ public static CustomerBarCode Create(string zipCode, string townAddress, string /// 郵便番号(半角数字7桁) internal static string CheckPostCode(string zipCode) { - // 全角→半角 - zipCode = string.Join("", zipCode.Select(ToHankaku)); - - // ハイフン除去 - zipCode = zipCode.Replace("-", ""); - - // \d は全角以外の Unicode 数字にも、\Z は末尾の改行にもマッチするため使わない - if (!Regex.IsMatch(zipCode, @"\A[0-9]{7}\z")) + // 全角→半角にし、ハイフンを除いて半角数字(0~9 のみ。全角以外の Unicode 数字や改行は不可)が7桁であること + var digits = new char[7]; + var count = 0; + foreach (var c in zipCode) { - throw new ArgumentException("郵便番号は7桁の数値で入力してください。"); + var hankaku = ToHankaku(c); + if (hankaku == '-') continue; + if (hankaku < '0' || hankaku > '9' || count == digits.Length) throw InvalidPostCode(); + digits[count++] = hankaku; } + if (count != digits.Length) throw InvalidPostCode(); - return zipCode; + return new string(digits); + } + + private static ArgumentException InvalidPostCode() + { + return new ArgumentException("郵便番号は7桁の数値で入力してください。"); } /// @@ -273,14 +278,76 @@ private static string ExtractAddressNumber(string address) /// 算用数字 internal static string KanjiToNumber(string kanji) { - if (kanji.IndexOfAny(new[] { '十', '百', '千' }) < 0) + return KanjiToNumber(kanji, 0, kanji.Length); + } + + /// + /// 文字列の一部の漢数字を算用数字に変換する + /// + /// 文字列 + /// 漢数字の開始位置 + /// 漢数字の長さ + /// 算用数字 + internal static string KanjiToNumber(string value, int start, int length) + { + if (!HasKanjiUnit(value, start, length)) + { + var digits = new char[length]; + for (int i = 0; i < length; i++) digits[i] = KanjiDigit(value[start + i]); + return new string(digits); + } + return KanjiValue(value, start, length).ToString(CultureInfo.InvariantCulture); + } + +#if NET8_0_OR_GREATER + /// + /// 文字列の一部の漢数字を算用数字に変換して書き込む + /// + /// 文字列 + /// 漢数字の開始位置 + /// 漢数字の長さ + /// 書き込み先(漢数字の長さの4倍あれば足りる) + /// 書き込んだ文字数 + internal static int WriteKanjiNumber(string value, int start, int length, Span destination) + { + if (!HasKanjiUnit(value, start, length)) { - return string.Concat(kanji.Select(c => KanjiDigits.IndexOf(c))); + for (int i = 0; i < length; i++) destination[i] = KanjiDigit(value[start + i]); + return length; } + KanjiValue(value, start, length).TryFormat(destination, out var written, default, CultureInfo.InvariantCulture); + return written; + } +#endif + + // 位取りの漢数字 + private static readonly char[] KanjiUnits = { '十', '百', '千' }; + + /// + /// 位取り(十、百、千)を含むかどうか(含まない場合は「二〇五」のように1文字ずつ数字にする) + /// + private static bool HasKanjiUnit(string value, int start, int length) + { + return value.IndexOfAny(KanjiUnits, start, length) >= 0; + } + /// + /// 位取りなしの漢数字1文字を算用数字にする + /// + private static char KanjiDigit(char c) + { + return (char)('0' + KanjiDigits.IndexOf(c)); + } + + /// + /// 位取りを含む漢数字(「十一」「千二百三十四」)の値 + /// + private static int KanjiValue(string value, int start, int length) + { int total = 0, current = 0; - foreach (var c in kanji) + for (int i = start; i < start + length; i++) { + var c = value[i]; var digit = KanjiDigits.IndexOf(c); if (digit >= 0) { @@ -291,7 +358,7 @@ internal static string KanjiToNumber(string kanji) total += (current == 0 ? 1 : current) * unit; current = 0; } - return (total + current).ToString(CultureInfo.InvariantCulture); + return total + current; } /// diff --git a/ZipConvertCustomerBarCode/ZipCodeDictionary.cs b/ZipConvertCustomerBarCode/ZipCodeDictionary.cs index 573309b..f57b329 100644 --- a/ZipConvertCustomerBarCode/ZipCodeDictionary.cs +++ b/ZipConvertCustomerBarCode/ZipCodeDictionary.cs @@ -229,11 +229,18 @@ internal static bool TrySplitAddress(ZipCodeEntry[] list, string address, out st blockAddress = null; // 最も長く一致した町域名で分割する + // (候補の文字列をつなげて作らないよう、都道府県名・市区町村名・町域名の順に照合する) var matcher = new AddressMatcher(address); - var end = list.SelectMany(x => x.GetTownAddressCandidates()) - .Select(x => matcher.MatchPrefix(x)) - .DefaultIfEmpty(-1) - .Max(); + var end = -1; + foreach (var entry in list) + { + // 「以下に掲載がない場合」は町域名がわからないため分割しない + if (entry.IsRepresentative) continue; + + var cityEnd = MatchCityName(entry, matcher); + if (cityEnd < 0) continue; + end = Math.Max(end, MatchTownName(entry, matcher, cityEnd, TownStarts(matcher, cityEnd))); + } if (end < 0) return false; townAddress = address.Substring(0, end).Trim(); @@ -387,6 +394,24 @@ private static bool TryGetSingleZipCode(IEnumerable found, out str return (matched, bestEnd); } + /// + /// 住所の先頭が市区町村名まで(都道府県名ありとなし、郡名ありとなし)と一致する場合、一致した部分の終わりの位置を返す + /// + /// 町域・事業所 + /// 住所 + /// 一致した部分の終わりの位置。一致しない場合は -1 + private static int MatchCityName(ZipCodeEntry entry, AddressMatcher matcher) + { + var prefectureEnd = matcher.MatchPrefix(entry.Prefecture, 0); + var end = -1; + foreach (var start in prefectureEnd >= 0 ? new[] { 0, prefectureEnd } : new[] { 0 }) + { + end = Math.Max(end, matcher.MatchPrefix(entry.City, start)); + if (entry.CityWithoutDistrict != null) end = Math.Max(end, matcher.MatchPrefix(entry.CityWithoutDistrict, start)); + } + return end; + } + /// /// 町域名(地区名)の照合を始める位置(後ろに「大字」「字」がある場合はその後ろも) /// @@ -668,14 +693,14 @@ public void ReadTowns(Stream stream) const string expected = "UTF-8形式の utf_ken_all.csv"; ReadLines(stream, new UTF8Encoding(false), (fields, lineNumber, line) => { - if (fields.Length != TownColumnCount) throw InvalidFormat(name, lineNumber, "列数が15ではありません", expected); + if (fields.Count != TownColumnCount) throw InvalidFormat(name, lineNumber, "列数が15ではありません", expected); // Shift_JIS のファイルを UTF-8 として読むと置換文字になる if (line.IndexOf((char)0xFFFD) >= 0) throw InvalidFormat(name, lineNumber, "UTF-8 として読めない文字があります", expected); - var zipCode = CheckZipCode(Unquote(fields[2]), name, lineNumber, expected); + var zipCode = CheckZipCode(fields.Get(2), name, lineNumber, expected); // 7:都道府県名 8:市区町村名 9:町域名(漢字) 12:丁目を有する町域 - var prefecture = CheckPrefecture(Unquote(fields[6]), name, lineNumber, expected); - Add(new ZipCodeEntry(zipCode, prefecture, Share(Unquote(fields[7])), Unquote(fields[8]), fields[11] == "1")); + var prefecture = CheckPrefecture(fields.Get(6), name, lineNumber, expected); + Add(new ZipCodeEntry(zipCode, prefecture, Share(fields.Get(7)), fields.Get(8), fields.Is(11, "1"))); }); } @@ -688,24 +713,22 @@ public void ReadBusinesses(Stream stream, Encoding encoding) const string expected = "JIGYOSYO.CSV"; ReadLines(stream, encoding, (fields, lineNumber, line) => { - if (fields.Length != BusinessColumnCount) throw InvalidFormat(name, lineNumber, "列数が13ではありません", expected); + if (fields.Count != BusinessColumnCount) throw InvalidFormat(name, lineNumber, "列数が13ではありません", expected); // 13:修正コード(5は廃止) - if (fields[12] == "5") return; + if (fields.Is(12, "5")) return; - var zipCode = CheckZipCode(Unquote(fields[7]), name, lineNumber, expected); + var zipCode = CheckZipCode(fields.Get(7), name, lineNumber, expected); // 11:個別番号の種別(0:大口事業所 1:私書箱) ZipCodeKind kind; - switch (fields[10]) - { - case "0": kind = ZipCodeKind.Business; break; - case "1": kind = ZipCodeKind.PostOfficeBox; break; - default: throw InvalidFormat(name, lineNumber, "個別番号の種別が0か1ではありません", expected); - } + if (fields.Is(10, "0")) kind = ZipCodeKind.Business; + else if (fields.Is(10, "1")) kind = ZipCodeKind.PostOfficeBox; + else throw InvalidFormat(name, lineNumber, "個別番号の種別が0か1ではありません", expected); + // 3:大口事業所名(漢字) 4:都道府県名 5:市区町村名 6:町域名 7:小字名、丁目、番地等 // 文字コードが違う(UTF-8 に変換したファイルなど)と、置換文字にならずに別の文字に化けるため、都道府県名で確認する - var prefecture = CheckPrefecture(Unquote(fields[3]), name, lineNumber, expected); - Add(new ZipCodeEntry(zipCode, prefecture, Share(Unquote(fields[4])), Unquote(fields[5]), Unquote(fields[6]), Unquote(fields[2]), kind)); + var prefecture = CheckPrefecture(fields.Get(3), name, lineNumber, expected); + Add(new ZipCodeEntry(zipCode, prefecture, Share(fields.Get(4)), fields.Get(5), fields.Get(6), fields.Get(2), kind)); }); } @@ -737,8 +760,12 @@ private string Share(string value) /// /// CSVを1行ずつ読み込む(各項目は","区切りで、文字列項目は「"」で囲まれている。項目内に","はない) /// - private static void ReadLines(Stream stream, Encoding encoding, Action read) + private static void ReadLines(Stream stream, Encoding encoding, Action read) { +#if NET8_0_OR_GREATER + // 区切り位置は行ごとに使い回す(列数が多すぎる場合を検出できるよう、最大の列数 + 1) + var ranges = new Range[Math.Max(TownColumnCount, BusinessColumnCount) + 1]; +#endif using (var reader = new StreamReader(stream, encoding, false, 4096, true)) { string line; @@ -747,7 +774,11 @@ private static void ReadLines(Stream stream, Encoding encoding, Action - /// 「"」で囲まれた項目から「"」を除く - /// - private static string Unquote(string field) - { - return field.Length >= 2 && field[0] == '"' && field[field.Length - 1] == '"' ? field.Substring(1, field.Length - 2) : field; - } - /// /// 形式エラー /// @@ -786,5 +809,59 @@ private static InvalidDataException InvalidFormat(string name, int lineNumber, s return new InvalidDataException($"{name}の{lineNumber}行目の形式が正しくありません({reason})。{expected} を指定してください。"); } } + + /// + /// CSVの1行の項目(各項目は","区切りで、文字列項目は「"」で囲まれている。項目内に","はない) + /// + private readonly struct CsvFields + { +#if NET8_0_OR_GREATER + // 使う項目だけを文字列にするため、行は区切り位置だけを求めておく + private readonly string line; + private readonly Range[] ranges; + + public CsvFields(string line, Range[] ranges) + { + this.line = line; + this.ranges = ranges; + Count = line.AsSpan().Split(ranges, ','); + } + + /// 項目数(最大の列数を超える場合は最大の列数 + 1) + public int Count { get; } + + /// 項目(「"」は除く) + public string Get(int index) => Unquote(line.AsSpan()[ranges[index]]).ToString(); + + /// 項目(「"」は除く)が指定した値かどうか + public bool Is(int index, string value) => Unquote(line.AsSpan()[ranges[index]]).SequenceEqual(value); + + private static ReadOnlySpan Unquote(ReadOnlySpan field) + { + return field.Length >= 2 && field[0] == '"' && field[field.Length - 1] == '"' ? field.Slice(1, field.Length - 2) : field; + } +#else + private readonly string[] fields; + + public CsvFields(string line) + { + fields = line.Split(','); + } + + /// 項目数 + public int Count => fields.Length; + + /// 項目(「"」は除く) + public string Get(int index) => Unquote(fields[index]); + + /// 項目(「"」は除く)が指定した値かどうか + public bool Is(int index, string value) => Get(index) == value; + + private static string Unquote(string field) + { + return field.Length >= 2 && field[0] == '"' && field[field.Length - 1] == '"' ? field.Substring(1, field.Length - 2) : field; + } +#endif + } } } diff --git a/ZipConvertCustomerBarCode/ZipCodeEntry.cs b/ZipConvertCustomerBarCode/ZipCodeEntry.cs index 9bb01d5..441d505 100644 --- a/ZipConvertCustomerBarCode/ZipCodeEntry.cs +++ b/ZipConvertCustomerBarCode/ZipCodeEntry.cs @@ -286,40 +286,38 @@ internal static string NormalizeBusinessName(string name) return normalized; } + // 郡名を除いた市区町村名(初めて使うときに作る。郡名がない場合は空) + private string cityWithoutDistrict; + /// - /// 市区町村名までの住所の候補 - /// 都道府県名ありとなし、郡名ありとなし + /// 郡名を除いた市区町村名(「上川郡東神楽町」の「東神楽町」)。郡名がない場合は null /// - internal IEnumerable GetCityCandidates() + internal string CityWithoutDistrict { - yield return Prefecture + City; - yield return City; - - var district = DistrictRegex.Match(City); - if (district.Success) + get { - yield return Prefecture + district.Groups["name"].Value; - yield return district.Groups["name"].Value; + if (cityWithoutDistrict == null) + { + var district = DistrictRegex.Match(City); + cityWithoutDistrict = district.Success ? district.Groups["name"].Value : ""; + } + return cityWithoutDistrict.Length > 0 ? cityWithoutDistrict : null; } } /// - /// 住所A(町域名までの住所)の候補 - /// 市区町村名までの住所の候補 + 町域名(前に「大字」「字」ありとなし。郵便番号データの町域名には付いていない) + /// 市区町村名までの住所の候補 + /// 都道府県名ありとなし、郡名ありとなし /// - internal IEnumerable GetTownAddressCandidates() + internal IEnumerable GetCityCandidates() { - // 「以下に掲載がない場合」は町域名がわからないため候補なし - if (IsRepresentative) yield break; + yield return Prefecture + City; + yield return City; - var town = GetTownName(); - var towns = town.Length == 0 ? new[] { town } : new[] { town, "大字" + town, "字" + town }; - foreach (var city in GetCityCandidates()) + if (CityWithoutDistrict != null) { - foreach (var t in towns) - { - yield return city + t; - } + yield return Prefecture + CityWithoutDistrict; + yield return CityWithoutDistrict; } } } diff --git a/ZipConvertCustomerBarCode/ZipConvertCustomerBarCode.csproj b/ZipConvertCustomerBarCode/ZipConvertCustomerBarCode.csproj index b151451..cea720f 100644 --- a/ZipConvertCustomerBarCode/ZipConvertCustomerBarCode.csproj +++ b/ZipConvertCustomerBarCode/ZipConvertCustomerBarCode.csproj @@ -1,7 +1,8 @@  - netstandard2.0 + + netstandard2.0;net8.0 郵便番号・住所から日本郵便のカスタマバーコードを生成する(描画ライブラリ非依存) true