< Summary

Information
Class: OzonDomains.Nomenclature.ParsedProductRef
Assembly: OzonDomains
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonDomains/Nomenclature/ProductKeyParser.cs
Line coverage
100%
Covered lines: 13
Uncovered lines: 0
Coverable lines: 13
Total lines: 171
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_ArticleRaw()100%11100%
get_ArticleClean()100%11100%
get_ArticleKey()100%11100%
get_ProductKeyArticleRaw()100%11100%
get_ProductKeyArticleClean()100%11100%
get_ProductKeyArticleKey()100%11100%
get_HasArticleConflict()100%11100%
get_BrandPrefix()100%11100%
get_BatchSize()100%11100%
get_BatchSizeFromArticle()100%11100%
get_VariantTag()100%11100%
get_Flags()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
 9535public sealed record ParsedProductRef(
 336    string ArticleRaw,
 7737    string ArticleClean,
 18438    string ArticleKey,
 339    string ProductKeyArticleRaw,
 540    string ProductKeyArticleClean,
 441    string ProductKeyArticleKey,
 5342    bool HasArticleConflict,
 7543    string BrandPrefix,
 1544    int BatchSize,
 845    int BatchSizeFromArticle,
 746    string? VariantTag,
 10847    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    {
 68        if (string.IsNullOrEmpty(productKey))
 69            return 1;
 70
 71        var match = NumericTag().Match(productKey);
 72        return match.Success && int.TryParse(match.Groups[1].Value, out var multiplier) && multiplier > 0
 73            ? multiplier
 74            : 1;
 75    }
 76
 77    public static ParsedProductRef Parse(string? article, string? productKey)
 78    {
 79        var raw = article?.Trim() ?? string.Empty;
 80        var flags = ProductRefFlags.None;
 81        var batchFromArticle = 1;
 82        string? variantTag = null;
 83
 84        var clean = raw;
 85        var eq = clean.IndexOf('=');
 86        string brandTail = string.Empty;
 87        if (eq >= 0)
 88        {
 89            brandTail = clean[(eq + 1)..];
 90            clean = clean[..eq];
 91            if (brandTail.Trim('=', '$', ' ').Length > 0)
 92                flags |= ProductRefFlags.ArticleHadBrandTail;
 93            else
 94                flags |= ProductRefFlags.ArticleHadJunk;
 95        }
 96
 97        foreach (Match tag in AnyTag().Matches(clean))
 98        {
 99            flags |= ProductRefFlags.ArticleHadBatchTag;
 100            var text = tag.Groups[1].Value.Trim();
 101            if (int.TryParse(text, out var n) && n > 0)
 102                batchFromArticle = n;
 103            else if (text.Length > 0)
 104                variantTag ??= text;
 105        }
 106
 107        clean = AnyTag().Replace(clean, string.Empty).Trim().TrimEnd('=', '$', ' ');
 108        if (clean.Length != raw.Length && !flags.HasFlag(ProductRefFlags.ArticleHadBrandTail) && !flags.HasFlag(ProductR
 109            flags |= ProductRefFlags.ArticleHadJunk;
 110
 111        var keyArticle = string.Empty;
 112        var brand = string.Empty;
 113        var key = productKey?.Trim() ?? string.Empty;
 114        if (key.Length > 0)
 115        {
 116            var keyEq = key.IndexOf('=');
 117            keyArticle = keyEq >= 0 ? key[..keyEq] : key;
 118            var afterEq = keyEq >= 0 ? key[(keyEq + 1)..] : string.Empty;
 119            var bracket = afterEq.IndexOf('[');
 120            brand = (bracket >= 0 ? afterEq[..bracket] : afterEq).Trim();
 121
 122            variantTag ??= FirstNonNumericTag(key);
 123        }
 124
 125        if (brand.Length == 0 && brandTail.Length > 0)
 126            brand = new string(brandTail.TakeWhile(char.IsLetterOrDigit).ToArray());
 127
 128        if (clean.Length == 0 && keyArticle.Length > 0)
 129            clean = AnyTag().Replace(keyArticle, string.Empty).Trim();
 130
 131        if (clean.Any(IsCyrillicLookalike))
 132            flags |= ProductRefFlags.ArticleHadCyrillicLookalike;
 133
 134        var cleanKeyArticle = AnyTag().Replace(keyArticle, string.Empty).Trim();
 135        // В части старых ключей код бренда продублирован прямо после артикула
 136        // (128497103SZK=SZK); перед сравнением он не считается частью артикула.
 137        if (brand.Length > 0 && cleanKeyArticle.EndsWith(brand, StringComparison.OrdinalIgnoreCase)
 138                             && cleanKeyArticle.Length > brand.Length)
 139            cleanKeyArticle = cleanKeyArticle[..^brand.Length];
 140        var keyArticleKey = ArticleKey.Key(cleanKeyArticle);
 141        return new ParsedProductRef(
 142            raw,
 143            clean,
 144            ArticleKey.Key(clean),
 145            keyArticle,
 146            cleanKeyArticle,
 147            keyArticleKey,
 148            brand.Length > 0 && clean.Length > 0 && keyArticleKey.Length > 0 && ArticleKey.Key(clean) != keyArticleKey,
 149            brand.ToUpperInvariant(),
 150            BatchMultiplier(key),
 151            batchFromArticle,
 152            variantTag,
 153            flags);
 154    }
 155
 156    private static string? FirstNonNumericTag(string key)
 157    {
 158        foreach (Match tag in AnyTag().Matches(key))
 159        {
 160            var text = tag.Groups[1].Value.Trim();
 161            if (text.Length > 0 && !int.TryParse(text, out _))
 162                return text;
 163        }
 164
 165        return null;
 166    }
 167
 168    private static bool IsCyrillicLookalike(char ch)
 169        => ch is 'А' or 'В' or 'Е' or 'К' or 'М' or 'Н' or 'О' or 'Р' or 'С' or 'Т' or 'Х' or 'У'
 170            or 'а' or 'в' or 'е' or 'к' or 'м' or 'н' or 'о' or 'р' or 'с' or 'т' or 'х' or 'у';
 171}