diff --git a/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs new file mode 100644 index 0000000..4a5babe --- /dev/null +++ b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Mvc; +using RhSolutions.SkuParser.Models; +using RhSolutions.SkuParser.Services; + +namespace RhSolutions.SkuParser.Controllers; + +[ApiController] +[Route("/api/[controller]")] +public class ProductsController : ControllerBase +{ + private IServiceProvider _provider; + private Dictionary _result; + public ProductsController(IServiceProvider provider) + { + _provider = provider; + _result = new(); + } + + [HttpPost] + public async Task PostFiles() + { + IFormFileCollection files = Request.Form.Files; + try + { + foreach (var file in files) + { + ISkuParser parser = _provider.GetRequiredKeyedService(file.ContentType); + IEnumerable productQuantities = await parser.ParseProducts(file); + foreach (ProductQuantity pq in productQuantities) + { + if (_result.ContainsKey(pq.Product)) + { + _result[pq.Product] += pq.Quantity; + } + else + { + _result.Add(pq.Product, pq.Quantity); + } + } + } + } + catch (Exception ex) + { + return BadRequest(error: $"{ex.Message}\n\n{ex.Source}\n{ex.StackTrace}"); + } + return new JsonResult(_result.Select(x => new { Sku = x.Key.ToString(), Quantity = x.Value })); + } +} \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Models/Product.cs b/RhSolutions.SkuParser.Api/Models/Product.cs new file mode 100644 index 0000000..fd9ea45 --- /dev/null +++ b/RhSolutions.SkuParser.Api/Models/Product.cs @@ -0,0 +1,65 @@ +using System.Text.RegularExpressions; + +namespace RhSolutions.SkuParser.Models; + +public record Product +{ + /// + /// Артикул РЕХАУ в заданном формате + /// + public required string Sku + { + get => _sku; + set + { + _sku = IsValudSku(value) + ? value + : throw new ArgumentException("$Неверный артикул: {value}"); + } + } + private string _sku = string.Empty; + private const string _parsePattern = @"(?[1\s]|^|\b)(?
\d{6})(?[\s13-])(?\d{3})(\b|$)"; + private const string _validnessPattern = @"^1\d{6}[1|3]\d{3}$"; + + private static bool IsValudSku(string value) + { + return Regex.IsMatch(value.Trim(), _validnessPattern); + } + private static string GetSku(Match match) + { + string lead = match.Groups["Lead"].Value; + string article = match.Groups["Article"].Value; + string delimiter = match.Groups["Delimiter"].Value; + string variant = match.Groups["Variant"].Value; + + if (lead != "1" && delimiter == "-") + { + return $"1{article}1{variant}"; + } + else + { + return $"{lead}{article}{delimiter}{variant}"; + } + } + + /// + /// Проверка строки на наличие в ней артикула РЕХАУ + /// + /// Входная строка для проверки + /// Артикул, если найден. null - если нет + /// Если артикул в строке есть возвращает true, Если нет - false + public static bool TryParse(string value, out Product? product) + { + product = null; + MatchCollection matches = Regex.Matches(value, _parsePattern); + if (matches.Count == 0) + { + return false; + } + string sku = GetSku(matches.First()); + product = new Product() { Sku = sku }; + return true; + } + public override int GetHashCode() => Sku.GetHashCode(); + public override string ToString() => Sku; +} \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs new file mode 100644 index 0000000..f5c5cbd --- /dev/null +++ b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs @@ -0,0 +1,11 @@ +using CsvHelper.Configuration.Attributes; + +namespace RhSolutions.SkuParser.Models; + +public class ProductQuantity +{ + [Index(0)] + public required Product Product { get; set; } + [Index(1)] + public required double Quantity { get; set; } +} diff --git a/RhSolutions.SkuParser.Api/Program.cs b/RhSolutions.SkuParser.Api/Program.cs index 1760df1..e3b3009 100644 --- a/RhSolutions.SkuParser.Api/Program.cs +++ b/RhSolutions.SkuParser.Api/Program.cs @@ -1,6 +1,9 @@ +using RhSolutions.SkuParser.Services; + var builder = WebApplication.CreateBuilder(args); +builder.Services.AddKeyedScoped("text/csv"); +builder.Services.AddControllers(); + var app = builder.Build(); - -app.MapGet("/", () => "Hello World!"); - -app.Run(); +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj b/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj index 1b28a01..d6e06a1 100644 --- a/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj +++ b/RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj @@ -6,4 +6,9 @@ enable + + + + + diff --git a/RhSolutions.SkuParser.Api/Services/CsvParser.cs b/RhSolutions.SkuParser.Api/Services/CsvParser.cs new file mode 100644 index 0000000..7f4f7c3 --- /dev/null +++ b/RhSolutions.SkuParser.Api/Services/CsvParser.cs @@ -0,0 +1,28 @@ +using System.Globalization; +using CsvHelper; +using CsvHelper.Configuration; +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; + +/// +/// Парсер артикулов и их количества из файлов *.csv +/// +public class CsvParser : ISkuParser +{ + public async Task> ParseProducts(IFormFile file) + { + using MemoryStream memoryStream = new(new byte[file.Length]); + await file.CopyToAsync(memoryStream); + memoryStream.Position = 0; + using StreamReader reader = new(memoryStream); + + var config = new CsvConfiguration(CultureInfo.GetCultureInfo("ru-RU")) + { + HasHeaderRecord = false, + }; + using CsvReader csvReader = new(reader, config); + + return csvReader.GetRecords().ToList(); + } +} diff --git a/RhSolutions.SkuParser.Api/Services/ExcelParser.cs b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs new file mode 100644 index 0000000..e6389bb --- /dev/null +++ b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs @@ -0,0 +1,11 @@ +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; + +public class ExcelParser : ISkuParser +{ + public Task> ParseProducts(IFormFile file) + { + throw new NotImplementedException(); + } +} diff --git a/RhSolutions.SkuParser.Api/Services/ISkuParser.cs b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs new file mode 100644 index 0000000..11ab243 --- /dev/null +++ b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs @@ -0,0 +1,7 @@ +using RhSolutions.SkuParser.Models; + +namespace RhSolutions.SkuParser.Services; +public interface ISkuParser +{ + public Task> ParseProducts(IFormFile file); +} diff --git a/RhSolutions.SkuParser.Tests/GlobalUsings.cs b/RhSolutions.SkuParser.Tests/GlobalUsings.cs new file mode 100644 index 0000000..139a90f --- /dev/null +++ b/RhSolutions.SkuParser.Tests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using NUnit.Framework; +global using RhSolutions.SkuParser.Models; \ No newline at end of file diff --git a/RhSolutions.SkuParser.Tests/ProductTests.cs b/RhSolutions.SkuParser.Tests/ProductTests.cs new file mode 100644 index 0000000..8d64e57 --- /dev/null +++ b/RhSolutions.SkuParser.Tests/ProductTests.cs @@ -0,0 +1,74 @@ +namespace RhSolutions.SkuParser.Tests; + +public class ProductTests +{ + [TestCase("12222221001")] + [TestCase("12222223001")] + [TestCase("160001-001")] + public void SimpleParse(string value) + { + Assert.True(Product.TryParse(value, out _)); + } + + [TestCase("string 12222221001")] + [TestCase("12222223001 string")] + [TestCase("string 160001-001")] + [TestCase("160001-001 string ")] + public void AdvancedParse(string value) + { + Assert.True(Product.TryParse(value, out _)); + } + + [TestCase("11600011001")] + [TestCase("160001-001")] + public void ProductIsCorrect(string value) + { + if (Product.TryParse(value, out Product? product)) + { + Assert.That(product!.Sku, Is.EqualTo("11600011001")); + } + else + { + Assert.Fail($"Parsing failed on {value}"); + } + } + + [TestCase("1222222001")] + [TestCase("12222225001")] + public void NotParses(string value) + { + Assert.False(Product.TryParse(value, out _)); + } + + [Test] + public void ProductEquality() + { + string value = "12222223001"; + Product.TryParse(value, out Product? first); + Product.TryParse(value, out Product? second); + if (first == null || second == null) + { + Assert.Fail($"Parsing failed on {value}"); + } + else + { + Assert.True(first.Equals(second)); + } + } + + [Test] + public void HashTest() + { + string value = "12222223001"; + HashSet set = new(); + if (Product.TryParse(value, out var product)) + { + set.Add(product!); + } + else + { + Assert.Fail($"Parsing failed on {value}"); + } + Assert.True(set.Contains(product!)); + } +} \ No newline at end of file diff --git a/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj b/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj new file mode 100644 index 0000000..5387543 --- /dev/null +++ b/RhSolutions.SkuParser.Tests/RhSolutions.SkuParser.Tests.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/RhSolutions.SkuParser.sln b/RhSolutions.SkuParser.sln index 65c9d38..05f155d 100644 --- a/RhSolutions.SkuParser.sln +++ b/RhSolutions.SkuParser.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Api", "RhSolutions.SkuParser.Api\RhSolutions.SkuParser.Api.csproj", "{5178E712-F984-48F4-9C68-6DC8CCAA4053}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Tests", "RhSolutions.SkuParser.Tests\RhSolutions.SkuParser.Tests.csproj", "{8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Debug|Any CPU.Build.0 = Debug|Any CPU {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.ActiveCfg = Release|Any CPU {5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.Build.0 = Release|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal