< Summary

Line coverage
97%
Covered lines: 71
Uncovered lines: 2
Coverable lines: 73
Total lines: 215
Line coverage: 97.2%
Branch coverage
95%
Covered branches: 114
Total branches: 120
Branch coverage: 95%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: BatchMultiplier(...)100%88100%
File 1: Parse(...)90%606096.49%
File 1: FirstNonNumericTag(...)100%66100%
File 1: IsCyrillicLookalike(...)100%4646100%
File 2: NumericTag()100%11100%
File 2: AnyTag()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonDomains/Nomenclature/ProductKeyParser.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2
 3namespace OzonDomains.Nomenclature;
 4
 5[Flags]
 6public enum ProductRefFlags
 7{
 8    None = 0,
 9
 10    /// <summary>В поле «Артикул» оказался хвост ключа: <c>1284971С03=SZK</c>, <c>19018211B=CRT=$</c>.</summary>
 11    ArticleHadBrandTail = 1,
 12
 13    /// <summary>В поле «Артикул» оказался тег в скобках: <c>813036[10]</c>, <c>5048-6[2]</c>.</summary>
 14    ArticleHadBatchTag = 2,
 15
 16    /// <summary>Замыкающие <c>=</c>/<c>$</c> и пробелы в артикуле.</summary>
 17    ArticleHadJunk = 4,
 18
 19    /// <summary>Кириллические двойники латиницы в артикуле (например, «С» в <c>1284971С03</c>).</summary>
 20    ArticleHadCyrillicLookalike = 8
 21}
 22
 23/// <summary>Разобранная ссылка на товар из заказа.</summary>
 24/// <param name="ArticleRaw">Артикул как пришёл (обрезан по краям).</param>
 25/// <param name="ArticleClean">Артикул без хвоста ключа, тегов в скобках и мусора; разделители сохранены.</param>
 26/// <param name="ArticleKey">Ключ поиска (<see cref="ArticleKey.Key"/>) от <see cref="ArticleClean"/>.</param>
 27/// <param name="ProductKeyArticleRaw">Артикульная часть <c>ProductKey</c> до <c>=</c>, как пришла из источника.</param>
 28/// <param name="ProductKeyArticleClean">Артикульная часть <c>ProductKey</c> без тегов партии.</param>
 29/// <param name="ProductKeyArticleKey">Нормализованный ключ артикульной части <c>ProductKey</c>.</param>
 30/// <param name="HasArticleConflict">Оба артикула заполнены, но после нормализации различаются.</param>
 31/// <param name="BrandPrefix">Трёхбуквенный код бренда из ключа (<c>ELR</c>), верхний регистр; пусто, если его нет.</par
 32/// <param name="BatchSize">Размер партии из <c>[N]</c> в ключе товара — тот же множитель, что в <c>Order.TrueQuantity</
 33/// <param name="BatchSizeFromArticle">Число из тега в самом поле «Артикул» (<c>813036[10]</c> → 10), 1 — если тега нет.
 34/// <param name="VariantTag">Нечисловой тег <c>[J2]</c>, <c>[V]</c>, <c>[b13]</c> — вариант листинга; не множитель.</par
 35public sealed record ParsedProductRef(
 36    string ArticleRaw,
 37    string ArticleClean,
 38    string ArticleKey,
 39    string ProductKeyArticleRaw,
 40    string ProductKeyArticleClean,
 41    string ProductKeyArticleKey,
 42    bool HasArticleConflict,
 43    string BrandPrefix,
 44    int BatchSize,
 45    int BatchSizeFromArticle,
 46    string? VariantTag,
 47    ProductRefFlags Flags);
 48
 49/// <summary>
 50/// Единый разбор ключа товара <c>АРТИКУЛ=БРЕНД[ТЕГ]</c> и поля «Артикул».
 51/// Число в скобках — размер партии (<c>122904=ELR[2]</c> — упаковка из двух колец одной карточки 1С),
 52/// нечисловое — вариант листинга. Чистые функции, зависимостей нет.
 53/// </summary>
 54public static partial class ProductKeyParser
 55{
 56    [GeneratedRegex(@"\[(\d+)\]")]
 57    private static partial Regex NumericTag();
 58
 59    [GeneratedRegex(@"\[([^\]]*)\]")]
 60    private static partial Regex AnyTag();
 61
 62    /// <summary>
 63    /// Множитель количества из первого <c>[N]</c> в ключе; 1, если тега нет, он нечисловой или N не больше нуля.
 64    /// Совпадает с прежним поведением <c>Order.ApplyProductKeyMultiplier</c> и резервов Bitrix.
 65    /// </summary>
 66    public static int BatchMultiplier(string? productKey)
 67    {
 66668        if (string.IsNullOrEmpty(productKey))
 1269            return 1;
 70
 65471        var match = NumericTag().Match(productKey);
 65472        return match.Success && int.TryParse(match.Groups[1].Value, out var multiplier) && multiplier > 0
 65473            ? multiplier
 65474            : 1;
 75    }
 76
 77    public static ParsedProductRef Parse(string? article, string? productKey)
 78    {
 9579        var raw = article?.Trim() ?? string.Empty;
 9580        var flags = ProductRefFlags.None;
 9581        var batchFromArticle = 1;
 9582        string? variantTag = null;
 83
 9584        var clean = raw;
 9585        var eq = clean.IndexOf('=');
 9586        string brandTail = string.Empty;
 9587        if (eq >= 0)
 88        {
 589            brandTail = clean[(eq + 1)..];
 590            clean = clean[..eq];
 591            if (brandTail.Trim('=', '$', ' ').Length > 0)
 492                flags |= ProductRefFlags.ArticleHadBrandTail;
 93            else
 194                flags |= ProductRefFlags.ArticleHadJunk;
 95        }
 96
 20697        foreach (Match tag in AnyTag().Matches(clean))
 98        {
 899            flags |= ProductRefFlags.ArticleHadBatchTag;
 8100            var text = tag.Groups[1].Value.Trim();
 8101            if (int.TryParse(text, out var n) && n > 0)
 8102                batchFromArticle = n;
 0103            else if (text.Length > 0)
 0104                variantTag ??= text;
 105        }
 106
 95107        clean = AnyTag().Replace(clean, string.Empty).Trim().TrimEnd('=', '$', ' ');
 95108        if (clean.Length != raw.Length && !flags.HasFlag(ProductRefFlags.ArticleHadBrandTail) && !flags.HasFlag(ProductR
 1109            flags |= ProductRefFlags.ArticleHadJunk;
 110
 95111        var keyArticle = string.Empty;
 95112        var brand = string.Empty;
 95113        var key = productKey?.Trim() ?? string.Empty;
 95114        if (key.Length > 0)
 115        {
 89116            var keyEq = key.IndexOf('=');
 89117            keyArticle = keyEq >= 0 ? key[..keyEq] : key;
 89118            var afterEq = keyEq >= 0 ? key[(keyEq + 1)..] : string.Empty;
 89119            var bracket = afterEq.IndexOf('[');
 89120            brand = (bracket >= 0 ? afterEq[..bracket] : afterEq).Trim();
 121
 89122            variantTag ??= FirstNonNumericTag(key);
 123        }
 124
 95125        if (brand.Length == 0 && brandTail.Length > 0)
 1126            brand = new string(brandTail.TakeWhile(char.IsLetterOrDigit).ToArray());
 127
 95128        if (clean.Length == 0 && keyArticle.Length > 0)
 1129            clean = AnyTag().Replace(keyArticle, string.Empty).Trim();
 130
 95131        if (clean.Any(IsCyrillicLookalike))
 2132            flags |= ProductRefFlags.ArticleHadCyrillicLookalike;
 133
 95134        var cleanKeyArticle = AnyTag().Replace(keyArticle, string.Empty).Trim();
 135        // В части старых ключей код бренда продублирован прямо после артикула
 136        // (128497103SZK=SZK); перед сравнением он не считается частью артикула.
 95137        if (brand.Length > 0 && cleanKeyArticle.EndsWith(brand, StringComparison.OrdinalIgnoreCase)
 95138                             && cleanKeyArticle.Length > brand.Length)
 2139            cleanKeyArticle = cleanKeyArticle[..^brand.Length];
 95140        var keyArticleKey = ArticleKey.Key(cleanKeyArticle);
 95141        return new ParsedProductRef(
 95142            raw,
 95143            clean,
 95144            ArticleKey.Key(clean),
 95145            keyArticle,
 95146            cleanKeyArticle,
 95147            keyArticleKey,
 95148            brand.Length > 0 && clean.Length > 0 && keyArticleKey.Length > 0 && ArticleKey.Key(clean) != keyArticleKey,
 95149            brand.ToUpperInvariant(),
 95150            BatchMultiplier(key),
 95151            batchFromArticle,
 95152            variantTag,
 95153            flags);
 154    }
 155
 156    private static string? FirstNonNumericTag(string key)
 157    {
 188158        foreach (Match tag in AnyTag().Matches(key))
 159        {
 7160            var text = tag.Groups[1].Value.Trim();
 7161            if (text.Length > 0 && !int.TryParse(text, out _))
 4162                return text;
 163        }
 164
 85165        return null;
 4166    }
 167
 168    private static bool IsCyrillicLookalike(char ch)
 538169        => ch is 'А' or 'В' or 'Е' or 'К' or 'М' or 'Н' or 'О' or 'Р' or 'С' or 'Т' or 'Х' or 'У'
 538170            or 'а' or 'в' or 'е' or 'к' or 'м' or 'н' or 'о' or 'р' or 'с' or 'т' or 'х' or 'у';
 171}

/home/runner/work/Studio2Prod/Studio2Prod/OzonDomains/obj/Release/net8.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/Studio2Prod/Studio2Prod/OzonDomains/obj/Release/net8.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).