148 lines
7.0 KiB
C#
148 lines
7.0 KiB
C#
using System.Text.RegularExpressions;
|
||
|
||
namespace Kit.Helpers.Validation
|
||
{
|
||
/// <summary>
|
||
/// Проверки форматов часто встречающихся реквизитов (ИНН, email, телефон, СНИЛС). Чистые
|
||
/// предикаты без побочных эффектов и текста ошибок: возвращают <c>true/false</c>, решение о
|
||
/// throw и формулировку сообщения принимает вызывающий код. Реализация — только BCL
|
||
/// (<see cref="Regex"/>, LINQ), без сторонних зависимостей. null/пустая строка → <c>false</c>
|
||
/// (обязательность и «пропуск пустого опционального поля» — забота вызывающего).
|
||
/// </summary>
|
||
public static class FormatCheck
|
||
{
|
||
// Прагматичная проверка email: одна @, непустые локальная и доменная части, точка в домене.
|
||
private static readonly Regex EmailRegex =
|
||
new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled);
|
||
|
||
// Веса контрольных разрядов ИНН (ФНС): 10-й разряд; 11-й и 12-й — для 12-значного.
|
||
private static readonly int[] InnWeights10 = { 2, 4, 10, 3, 5, 9, 4, 6, 8 };
|
||
private static readonly int[] InnWeights11 = { 7, 2, 4, 10, 3, 5, 9, 4, 6, 8 };
|
||
private static readonly int[] InnWeights12 = { 3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8 };
|
||
|
||
/// <summary>ИНН: только цифры, длина 10 (ЮЛ) или 12 (ИП/ФЛ), контрольная сумма сходится.</summary>
|
||
public static bool IsInn(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||
|
||
var digits = value.Trim();
|
||
if (!digits.All(char.IsDigit) || (digits.Length != 10 && digits.Length != 12))
|
||
return false;
|
||
|
||
return IsInnChecksumValid(digits);
|
||
}
|
||
|
||
/// <summary>Email по прагматичному шаблону (одна @, точка в домене), длина ≤ 254.</summary>
|
||
public static bool IsEmail(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||
|
||
var email = value.Trim();
|
||
return email.Length <= 254 && EmailRegex.IsMatch(email);
|
||
}
|
||
|
||
/// <summary>Телефон: 10–15 цифр; допустимы «+», пробел, дефис, скобки как разделители.</summary>
|
||
public static bool IsPhone(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||
|
||
var phone = value.Trim();
|
||
var hasInvalidChar = phone.Any(character =>
|
||
!char.IsDigit(character) && character is not ('+' or '-' or ' ' or '(' or ')'));
|
||
if (hasInvalidChar) return false;
|
||
|
||
var digitsCount = phone.Count(char.IsDigit);
|
||
return digitsCount is >= 10 and <= 15;
|
||
}
|
||
|
||
/// <summary>СНИЛС: 11 цифр (разделители игнорируются), контрольная сумма сходится.</summary>
|
||
public static bool IsSnils(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||
|
||
var digits = new string(value.Where(char.IsDigit).ToArray());
|
||
return digits.Length == 11 && IsSnilsChecksumValid(digits);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Строка идентификаторов для фильтров БД (формат fn_sys_range): целые числа
|
||
/// и диапазоны через «..» или «-», элементы через запятую, пробелы игнорируются.
|
||
/// Пример: «1,5,7..9, 11..15,19». У диапазона начало не больше конца.
|
||
/// </summary>
|
||
public static bool IsIdRange(string? value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||
|
||
// Строгий разбор: пустой элемент (двойная запятая, хвостовая запятая) недопустим.
|
||
var items = value.Split(',');
|
||
return items.All(item => IsIdRangeItemValid(item.Trim()));
|
||
}
|
||
|
||
// Элемент строки идентификаторов: «42», «7..9» или «7-9»; диапазон не обратный.
|
||
// Пробелы внутри элемента («2 .. 3») игнорируются.
|
||
private static bool IsIdRangeItemValid(string item)
|
||
{
|
||
item = item.Replace(" ", string.Empty);
|
||
if (item.Length == 0) return false;
|
||
|
||
var separatorIndex = item.IndexOf("..", StringComparison.Ordinal);
|
||
string lo;
|
||
string hi;
|
||
|
||
if (separatorIndex >= 0)
|
||
{
|
||
lo = item[..separatorIndex];
|
||
hi = item[(separatorIndex + 2)..];
|
||
}
|
||
else
|
||
{
|
||
var dashIndex = item.IndexOf('-', StringComparison.Ordinal);
|
||
if (dashIndex < 0) return IsPositiveInt(item);
|
||
|
||
lo = item[..dashIndex];
|
||
hi = item[(dashIndex + 1)..];
|
||
}
|
||
|
||
return IsPositiveInt(lo) && IsPositiveInt(hi) && int.Parse(lo) <= int.Parse(hi);
|
||
}
|
||
|
||
// Целое неотрицательное число без знака и ведущих «+».
|
||
private static bool IsPositiveInt(string value)
|
||
=> value.Length > 0 && value.All(char.IsDigit);
|
||
|
||
// Контрольная сумма ИНН: для 10-значного — один контрольный разряд, для 12-значного — два.
|
||
private static bool IsInnChecksumValid(string inn)
|
||
{
|
||
var digits = inn.Select(character => character - '0').ToArray();
|
||
|
||
if (digits.Length == 10)
|
||
return ControlDigit(digits, InnWeights10) == digits[9];
|
||
|
||
return ControlDigit(digits, InnWeights11) == digits[10]
|
||
&& ControlDigit(digits, InnWeights12) == digits[11];
|
||
}
|
||
|
||
private static int ControlDigit(int[] digits, int[] weights)
|
||
{
|
||
var sum = weights.Select((weight, index) => weight * digits[index]).Sum();
|
||
return sum % 11 % 10;
|
||
}
|
||
|
||
// Контрольная сумма СНИЛС: взвешенная сумма первых 9 цифр (веса 9..1) mod 101 → последние две.
|
||
private static bool IsSnilsChecksumValid(string snils)
|
||
{
|
||
var digits = snils.Select(character => character - '0').ToArray();
|
||
|
||
var sum = 0;
|
||
for (var i = 0; i < 9; i++)
|
||
sum += digits[i] * (9 - i);
|
||
|
||
var control = sum % 101;
|
||
if (control == 100) control = 0;
|
||
|
||
var provided = digits[9] * 10 + digits[10];
|
||
return control == provided;
|
||
}
|
||
}
|
||
}
|