VEU.WathBody/3-app/1-lib/Kit.Helpers/Validation/FormatCheck.cs

148 lines
7.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}