using System.Text.RegularExpressions;
namespace Kit.Helpers.Validation
{
///
/// Проверки форматов часто встречающихся реквизитов (ИНН, email, телефон, СНИЛС). Чистые
/// предикаты без побочных эффектов и текста ошибок: возвращают true/false, решение о
/// throw и формулировку сообщения принимает вызывающий код. Реализация — только BCL
/// (, LINQ), без сторонних зависимостей. null/пустая строка → false
/// (обязательность и «пропуск пустого опционального поля» — забота вызывающего).
///
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 };
/// ИНН: только цифры, длина 10 (ЮЛ) или 12 (ИП/ФЛ), контрольная сумма сходится.
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);
}
/// Email по прагматичному шаблону (одна @, точка в домене), длина ≤ 254.
public static bool IsEmail(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return false;
var email = value.Trim();
return email.Length <= 254 && EmailRegex.IsMatch(email);
}
/// Телефон: 10–15 цифр; допустимы «+», пробел, дефис, скобки как разделители.
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;
}
/// СНИЛС: 11 цифр (разделители игнорируются), контрольная сумма сходится.
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);
}
///
/// Строка идентификаторов для фильтров БД (формат fn_sys_range): целые числа
/// и диапазоны через «..» или «-», элементы через запятую, пробелы игнорируются.
/// Пример: «1,5,7..9, 11..15,19». У диапазона начало не больше конца.
///
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;
}
}
}