تحلیل و طراحی گامبهگام عبارت منظم (Regex) برای اعتبارسنجی کدپستی ایران
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۴/۳۱ ۰۸:۱۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
کدپستی در ایران با اعداد ۱،۳،۴،۵،۶،۷،۸،۹ شروع می شود. در پنج رقم اول این کد از ۰ و ۲ به دلیل شباهت با اعداد ۵ و ۳ استفاده نمی شود؛ ولی در پنج رقم دوم استفاده از این دو عدد بلامانع است. به پنج رقم اول کدپستی “گشت نامه رسانی” می گویند که به مشخصات شهر مقصد و محدوده جغرافیایی آن می پردازد. رقم اول به حوزه یعنی بزرگترین واحد تقسیمات پستی و رقم دوم به منطقه پستی اشاره می کند. سه رقم بعدی حوزه مکان مورد نظر را مشخص می کند. پنج رقم بعدی کدپستی به مشخصات مکانی مقصد می پردازد و دسترسی مستقیم به مقصد را ممکن می سازد.
0 یا 2 باشد. بنابراین، باید از مجموعهی [13456789] انتخاب شود.0 و 2 مجاز نیست.0 تا 9) مجاز است.^ برای شروع رشته و $ برای انتهای رشته استفاده میکنیم.[13-9] باشند. با استفاده از سور (Quantifier)، این بخش را به صورت [13-9]{5} مینویسیم.\d مشخص میشود. این بخش نیز به صورت \d{5} نوشته میشود.^[13-9]{5}\d{5}$(?!...) نمایش داده میشود و یک گزارهی صفر-پهنا (Zero-width assertion) است؛ یعنی بدون مصرف کردن کاراکتر، شرطی را در ادامهی رشته بررسی میکند و تنها در صورت عدم برقراری آن شرط، تطابق کلی موفقیتآمیز خواهد بود.(?!.*(.).*\1) است. تحلیل این عبارت:.*: این بخش با هر توالی از کاراکترها (از نقطهی فعلی تا انتهای رشته) تطبیق مییابد.(.): هر کاراکتری را تطبیق داده و آن را در گروه اول به حافظه میسپارد (Capturing Group 1).\1: این یک بازآوری (Backreference) به گروه اول است و با همان کاراکتری که در گروه اول ذخیره شده بود، تطبیق مییابد.(.) و \1 با یک فاصلهی .*، وجود یک کاراکتر تکراری را در رشته مشخص میکند. قرار دادن آن در یک Negative Lookahead، این شرط را معکوس کرده و تنها رشتههایی را میپذیرد که فاقد هرگونه کاراکتر تکراری باشند.^(?!.*(.).*\1)[13-9]{5}\d{5}$(.)\1 است. در این حالت، دیگر به .* میانی برای ایجاد فاصله نیازی نیست.(?!.*(.)\1). این عبارت تضمین میکند که در هیچ کجای رشته، یک کاراکتر و تکرار بلافصل آن وجود نداشته باشد.^(?!.*(.)\1)[13-9]{5}\d{5}$1345678901 را (که دارای تکرار غیرمتوالی است) معتبر تشخیص داده، اما رشتهی 1344567890 را (به دلیل تکرار متوالی 44) رد میکند:using System.Text.RegularExpressions;
namespace PostalCodeValidation
{
/// <summary>
/// شامل متدهای الحاقی برای اعتبارسنجی رشتهها
/// </summary>
public static class ValidationExtensions
{
// الگوی Regex به صورت استاتیک و فقط-خواندنی تعریف میشود تا تنها یک بار کامپایل و در حافظه نگهداری شود.
// این کار بهینگی را در فراخوانیهای متعدد افزایش میدهد.
private static readonly Regex IranianPostalCodeRegex = new Regex(@"^(?!.*(.)\1)[13-9]{5}\d{5}$");
/// <summary>
/// بررسی میکند که آیا رشتهی ورودی یک کد پستی معتبر ایرانی است یا خیر.
/// (بدون ارقام تکراری پشت سر هم)
/// </summary>
/// <param name="postalCode">رشتهی مورد بررسی به عنوان کد پستی.</param>
/// <returns>اگر کد پستی معتبر باشد true و در غیر این صورت false را برمیگرداند.</returns>
public static bool IsValidIranianPostalCode(this string postalCode)
{
// یک کد پستی معتبر نمیتواند null، خالی یا فقط شامل فضای خالی باشد.
if (string.IsNullOrWhiteSpace(postalCode))
{
return false;
}
return IranianPostalCodeRegex.IsMatch(postalCode);
}
}
}using Microsoft.VisualStudio.TestTools.UnitTesting;
using PostalCodeValidation; // اضافه کردن فضای نام کلاس الحاقی
namespace PostalCodeValidation.Tests
{
[TestClass]
public class IranianPostalCodeValidationTests
{
// این متد کدهای معتبر را بر اساس قوانین نهایی آزمون میکند.
[TestMethod]
public void IsValidIranianPostalCode_ValidCodes_ReturnsTrue()
{
// Arrange: آمادهسازی دادههای ورودی
string validCodeWithNonConsecutiveRepeat = "1345678901"; // تکرار غیرمتوالی مجاز است
string validCodeWithoutRepeat = "9876543210";
// Act: فراخوانی متد مورد آزمون
bool result1 = validCodeWithNonConsecutiveRepeat.IsValidIranianPostalCode();
bool result2 = validCodeWithoutRepeat.IsValidIranianPostalCode();
// Assert: بررسی نتیجهی مورد انتظار
Assert.IsTrue(result1, "کد با تکرار غیرمتوالی باید معتبر شناخته شود.");
Assert.IsTrue(result2, "کد بدون تکرار باید معتبر باشد.");
}
// این متد کدهایی را که دارای ارقام تکراری پشت سر هم هستند، آزمون میکند.
[TestMethod]
public void IsValidIranianPostalCode_WithConsecutiveRepeats_ReturnsFalse()
{
// Arrange
string codeWithConsecutiveRepeat1 = "1344567890"; // تکرار '44'
string codeWithConsecutiveRepeat2 = "3456788912"; // تکرار '88'
// Act
bool result1 = codeWithConsecutiveRepeat1.IsValidIranianPostalCode();
bool result2 = codeWithConsecutiveRepeat2.IsValidIranianPostalCode();
// Assert
Assert.IsFalse(result1, "کد با تکرار '44' باید نامعتبر باشد.");
Assert.IsFalse(result2, "کد با تکرار '88' باید نامعتبر باشد.");
}
// این متد کدهایی را که با ارقام غیرمجاز شروع شدهاند، آزمون میکند.
[TestMethod]
public void IsValidIranianPostalCode_InvalidStartingDigit_ReturnsFalse()
{
// Arrange
string codeStartingWithTwo = "2987654321";
// Act
bool result = codeStartingWithTwo.IsValidIranianPostalCode();
// Assert
Assert.IsFalse(result, "کد شروع شده با '2' باید نامعتبر باشد.");
}
// این متد کدهایی را که در پنج رقم اول ارقام غیرمجاز دارند، آزمون میکند.
[TestMethod]
public void IsValidIranianPostalCode_InvalidDigitInFirstBlock_ReturnsFalse()
{
// Arrange
string codeWithZeroInFirstBlock = "1045678932";
// Act
bool result = codeWithZeroInFirstBlock.IsValidIranianPostalCode();
// Assert
Assert.IsFalse(result, "کد دارای '0' در پنج رقم اول باید نامعتبر باشد.");
}
// این متد موارد مرزی مانند رشتهی null یا با طول اشتباه را آزمون میکند.
[TestMethod]
public void IsValidIranianPostalCode_EdgeCases_ReturnsFalse()
{
// Arrange
string nullString = null;
string emptyString = "";
string shortString = "12345";
// Act & Assert
Assert.IsFalse(nullString.IsValidIranianPostalCode(), "رشته null باید نامعتبر باشد.");
Assert.IsFalse(emptyString.IsValidIranianPostalCode(), "رشته خالی باید نامعتبر باشد.");
Assert.IsFalse(shortString.IsValidIranianPostalCode(), "رشته با طول کمتر از 10 باید نامعتبر باشد.");
}
}
}private static readonly Regex _matchIranianPostalCode = new(pattern: @"^(?!(\d)\1{3})[13-9]{5}\d{5}$",
RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexUtils.MatchTimeout);11 یا 55) به صورت پشت سر هم وجود نداشته باشند، سادهگیرتر است).| ویژگی | الگوی نهایی^(?!(\d)\1{3})[13-9]{5}\d{5}$ | الگوی پیشنهادی^(?!.*(.)\1)[13-9]{5}\d{5}$ |
| شرط بررسی تکرار | (?!(\d)\1{3}) این الگو بررسی میکند که کد با چهار رقم یکسان (مثلاً 1111) شروع نشود. | (?!.*(.)\1) این الگو بررسی میکند که هیچ دو رقم یکسانی (مثلاً 11 یا 55) به صورت پشت سر هم وجود نداشته باشد. |
| محدودهی بررسی تکرار | فقط ابتدای کد: شرط عدم تکرار را فقط برای چهار رقم اول کد بررسی میکند و کاری به بقیه ارقام ندارد. | تمام طول کد: شرط عدم تکرار را به لطف .* در تمام طول رشتهی ۱۰ رقمی بررسی میکند. |
برای ساختار کد پستی، باید در 5 رقم اول، حتما عدد 1 وجود داشته باشد و تا رقمی که شامل یک میشود، به عنوان کد شهر در نظر گرفته میشود؛ مثل 79146 که در آن، عدد 791، کد شهر و 46، کد گشت مربوط به آن شهر میباشد. ضمنا اگر در 5 رقم اول، به 5 ختم شود، این گشت مربوط به صندوق پستی میباشد و معمولا در این حالت بعلاوه 5 رقم، 3 یا 4 رقم میباشد؛ مثلا 79145-1315 که این یک آدرس صندوق شخصی است.
'1'الزامی است. این یک شرط منطقی جدید به اعتبارسنجی اضافه میکند.'5' باشد، آن کد مربوط به صندوق پستی است.XXXX5-XXX یا XXXX5-XXXX.// الگوی جدید و یکپارچه
private static readonly Regex IranianPostalCodeRegex = new Regex(
@"^(?!(\d)\1{3})(?=.{0,4}1)((?:[13-9]{0,4}1[13-9]{0,4})\d{5}|(?:[13-9]{4}5)-\d{3,4})$");
// گروه ضبط برای کد شهر. گروه 2 یا 3 بسته به فرمت کد.
// ((...)\d{5} | (...)-\d{3,4})
// ^گروه 2 ^گروه 3
private static readonly Regex CityCodeExtractorRegex = new Regex(
@"^(?!(\d)\1{3})(?=.{0,4}1)(([13-9]{0,4}1[13-9]{0,4})\d{5}|([13-9]{4}5)-\d{3,4})$");^...$ : تضمین میکند که کل رشته با الگو مطابقت دارد.(?!.*(.)\1) : (شرط عدم تکرار متوالی) این تطابق پیشنگر منفی (Negative Lookahead) مانند قبل، تضمین میکند که هیچ دو رقم یکسانی پشت سر هم قرار نگرفته باشند (که البته در این مورد، در نظر قبلی بیشتر بحث شد و حالت دیگری را هم میتوان درنظر گرفت: ?!(\d)\1{3}).(?=.{0,4}1) : (شرط اجباری بودن '1') این یک تطابق پیشنگر مثبت (Positive Lookahead) است. این بخش بدون اینکه کاراکتری را مصرف کند، بررسی میکند که آیا در ۴ کاراکتر بعدی (یعنی مجموعا در ۵ رقم اول)، کاراکتر '1' وجود دارد یا خیر. اگر وجود نداشته باشد، کل الگو رد میشود.( ... | ... ) : این ساختار OR منطقی است که به الگو اجازه میدهد یکی از دو فرمت ممکن را بپذیرد.([13-9]{0,4}1[13-9]{0,4})\d{5}([13-9]{0,4}1[13-9]{0,4}): این بخش ۵ رقم اول را با این تضمین که حتما شامل یک عدد '1' است، تطبیق میدهد و آن را در یک گروه ضبط (Capturing Group) برای استخراج بعدی قرار میدهد.\d{5}: پنج رقم دوم کد.([13-9]{4}5)-\d{3,4}([13-9]{4}5): چهار رقم از مجموعه مجاز و سپس عدد '5' که آن را نیز در یک گروه ضبط میکند.-: کاراکتر خط تیره.\d{3,4}: ۳ یا ۴ رقم برای شماره صندوق.using System.Text.RegularExpressions;
namespace PostalCodeValidation
{
public static class ValidationExtensions
{
// الگوی جدید و یکپارچه
private static readonly Regex IranianPostalCodeRegex = new Regex(
@"^(?!(\d)\1{3})(?=.{0,4}1)((?:[13-9]{0,4}1[13-9]{0,4})\d{5}|(?:[13-9]{4}5)-\d{3,4})$");
// گروه ضبط برای کد شهر. گروه 2 یا 3 بسته به فرمت کد.
// ((...)\d{5} | (...)-\d{3,4})
// ^گروه 2 ^گروه 3
private static readonly Regex CityCodeExtractorRegex = new Regex(
@"^(?!(\d)\1{3})(?=.{0,4}1)(([13-9]{0,4}1[13-9]{0,4})\d{5}|([13-9]{4}5)-\d{3,4})$");
/// <summary>
/// بررسی میکند که آیا رشته ورودی یک کد پستی معتبر ایرانی (فرمت استاندارد یا صندوق پستی) است.
/// </summary>
public static bool IsValidIranianPostalCode(this string postalCode)
{
if (string.IsNullOrWhiteSpace(postalCode))
{
return false;
}
return IranianPostalCodeRegex.IsMatch(postalCode);
}
/// <summary>
/// کد پستی را اعتبارسنجی کرده و در صورت اعتبار، کد شهر را استخراج میکند.
/// </summary>
/// <param name="postalCode">کد پستی ورودی.</param>
/// <param name="cityCode">خروجی کد شهر در صورت معتبر بودن کد پستی.</param>
/// <returns>اگر کد پستی معتبر باشد true و در غیر این صورت false.</returns>
public static bool TryParseIranianPostalCode(this string postalCode, out string cityCode)
{
cityCode = null;
if (string.IsNullOrWhiteSpace(postalCode))
{
return false;
}
Match match = CityCodeExtractorRegex.Match(postalCode);
if (match.Success)
{
// بر اساس الگو، کد شهر یا در گروه 3 (برای کد استاندارد) یا گروه 4 (برای کد صندوق پستی) قرار دارد.
// ما به دنبال اولین گروهی هستیم که با موفقیت ضبط شده باشد.
if (match.Groups[3].Success)
{
// کد شهر در فرمت استاندارد تا اولین '1' است
var firstBlock = match.Groups[3].Value;
cityCode = firstBlock.Substring(0, firstBlock.IndexOf('1') + 1);
}
else if (match.Groups[4].Success)
{
// کد شهر در فرمت صندوق پستی تا اولین '1' است
var firstBlock = match.Groups[4].Value;
cityCode = firstBlock.Substring(0, firstBlock.IndexOf('1') + 1);
}
return true;
}
return false;
}
}
}