aboutsummaryrefslogtreecommitdiff
path: root/RhSolutions.SkuParser.Api
diff options
context:
space:
mode:
authorSerghei Cebotari <serghei@cebotari.ru>2024-07-21 16:25:59 +0300
committerSerghei Cebotari <serghei@cebotari.ru>2024-07-21 16:25:59 +0300
commit6fe0a5e92b071411a0408d59d54d0de78e55d75c (patch)
tree3100d0991b67c17434d131e509eefc85f6a71ff8 /RhSolutions.SkuParser.Api
parente0313b83a033040660f0669de6d9e77042e87026 (diff)
Squashed commit of the following:
commit 688c5426e8793b808b9c75c9a19733af0a402fcb Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 16:25:14 2024 +0300 Switch to port 8080 commit c39249f6528ec76686a9382d1dc375c07d1d5044 Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 16:24:59 2024 +0300 Switch to alpine image commit 5318d7ec3f4f3d205549cf6732fa5b066a1d0a36 Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 15:40:14 2024 +0300 Add docker commit b6cd60a973da26bc92cf1fb45b4d2396b7ce56ea Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 15:00:12 2024 +0300 Delete asynchrony commit 44a194e6d27312f3b8dd0b9c9c02d873e06e0b22 Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 14:59:29 2024 +0300 Add Equals and GetHasCode methods overrides to ProductQuantity class commit a274eadd313e12f11cc84d32e5030bbc5b187f8c Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 14:58:37 2024 +0300 Add parsers tests commit 4f969e70d9716d8ddb4f4efedd466846289d7e2b Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sun Jul 21 14:57:55 2024 +0300 Update product tests commit 2485e20d0e93bed562f929055b6867dc2574a95b Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sat Jul 20 19:34:19 2024 +0300 Implement Excel parser commit 30f2e28c87a4d961c1f1fc48fbd72334905bf4ed Author: Serghei Cebotari <serghei@cebotari.ru> Date: Sat Jul 20 16:58:35 2024 +0300 Implement csv parser commit 08e86b43c0829de341dc3d24fbe01aadbed2e173 Author: Serghei Cebotari <serghei@cebotari.ru> Date: Thu Jul 18 21:01:28 2024 +0300 Edit port number
Diffstat (limited to 'RhSolutions.SkuParser.Api')
-rw-r--r--RhSolutions.SkuParser.Api/Controllers/ProductsController.cs48
-rw-r--r--RhSolutions.SkuParser.Api/Models/Product.cs65
-rw-r--r--RhSolutions.SkuParser.Api/Models/ProductQuantity.cs30
-rw-r--r--RhSolutions.SkuParser.Api/Program.cs14
-rw-r--r--RhSolutions.SkuParser.Api/Properties/launchSettings.json15
-rw-r--r--RhSolutions.SkuParser.Api/RhSolutions.SkuParser.Api.csproj5
-rw-r--r--RhSolutions.SkuParser.Api/Services/CsvParser.cs24
-rw-r--r--RhSolutions.SkuParser.Api/Services/ExcelParser.cs76
-rw-r--r--RhSolutions.SkuParser.Api/Services/ISkuParser.cs7
9 files changed, 268 insertions, 16 deletions
diff --git a/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs b/RhSolutions.SkuParser.Api/Controllers/ProductsController.cs
new file mode 100644
index 0000000..d85b01b
--- /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<Product, double> _result;
+ public ProductsController(IServiceProvider provider)
+ {
+ _provider = provider;
+ _result = new();
+ }
+
+ [HttpPost]
+ public IActionResult PostFiles()
+ {
+ IFormFileCollection files = Request.Form.Files;
+ try
+ {
+ foreach (var file in files)
+ {
+ ISkuParser parser = _provider.GetRequiredKeyedService<ISkuParser>(file.ContentType);
+ IEnumerable<ProductQuantity> productQuantities = 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
+{
+ /// <summary>
+ /// Артикул РЕХАУ в заданном формате
+ /// </summary>
+ public required string Sku
+ {
+ get => _sku;
+ set
+ {
+ _sku = IsValudSku(value)
+ ? value
+ : throw new ArgumentException("$Неверный артикул: {value}");
+ }
+ }
+ private string _sku = string.Empty;
+ private const string _parsePattern = @"(?<Lead>[1\s]|^|\b)(?<Article>\d{6})(?<Delimiter>[\s13-])(?<Variant>\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}";
+ }
+ }
+
+ /// <summary>
+ /// Проверка строки на наличие в ней артикула РЕХАУ
+ /// </summary>
+ /// <param name="value">Входная строка для проверки</param>
+ /// <param name="product">Артикул, если найден. null - если нет</param>
+ /// <returns>Если артикул в строке есть возвращает true, Если нет - false</returns>
+ 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..b7b154d
--- /dev/null
+++ b/RhSolutions.SkuParser.Api/Models/ProductQuantity.cs
@@ -0,0 +1,30 @@
+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; }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj == null || GetType() != obj.GetType())
+ {
+ return false;
+ }
+ ProductQuantity other = (ProductQuantity)obj;
+ return Product == other.Product &&
+ Quantity == other.Quantity;
+ }
+
+ public override int GetHashCode()
+ {
+ HashCode hash = new();
+ hash.Add(Product);
+ hash.Add(Quantity);
+ return hash.ToHashCode();
+ }
+}
diff --git a/RhSolutions.SkuParser.Api/Program.cs b/RhSolutions.SkuParser.Api/Program.cs
index 1760df1..44c642a 100644
--- a/RhSolutions.SkuParser.Api/Program.cs
+++ b/RhSolutions.SkuParser.Api/Program.cs
@@ -1,6 +1,12 @@
-var builder = WebApplication.CreateBuilder(args);
-var app = builder.Build();
+using RhSolutions.SkuParser.Services;
-app.MapGet("/", () => "Hello World!");
+var builder = WebApplication.CreateBuilder(args);
+builder.Services
+ .AddKeyedScoped<ISkuParser, CsvParser>("text/csv")
+ .AddKeyedScoped<ISkuParser, ExcelParser>("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+ .AddKeyedScoped<ISkuParser, ExcelParser>("application/vnd.ms-excel.sheet.macroenabled.12");
+builder.Services.AddControllers();
-app.Run();
+var app = builder.Build();
+app.MapControllers();
+app.Run(); \ No newline at end of file
diff --git a/RhSolutions.SkuParser.Api/Properties/launchSettings.json b/RhSolutions.SkuParser.Api/Properties/launchSettings.json
index 06b56be..09f4eae 100644
--- a/RhSolutions.SkuParser.Api/Properties/launchSettings.json
+++ b/RhSolutions.SkuParser.Api/Properties/launchSettings.json
@@ -4,7 +4,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
- "applicationUrl": "http://localhost:35100",
+ "applicationUrl": "http://localhost:8080",
"sslPort": 44355
}
},
@@ -12,17 +12,8 @@
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "http://localhost:5087",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "https": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "https://localhost:7266;http://localhost:5087",
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:8080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
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 @@
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="ClosedXML" Version="0.102.3" />
+ <PackageReference Include="CsvHelper" Version="33.0.1" />
+ </ItemGroup>
+
</Project>
diff --git a/RhSolutions.SkuParser.Api/Services/CsvParser.cs b/RhSolutions.SkuParser.Api/Services/CsvParser.cs
new file mode 100644
index 0000000..436a949
--- /dev/null
+++ b/RhSolutions.SkuParser.Api/Services/CsvParser.cs
@@ -0,0 +1,24 @@
+using System.Globalization;
+using CsvHelper;
+using CsvHelper.Configuration;
+using RhSolutions.SkuParser.Models;
+
+namespace RhSolutions.SkuParser.Services;
+
+/// <summary>
+/// Парсер артикулов и их количества из файлов *.csv
+/// </summary>
+public class CsvParser : ISkuParser
+{
+ public IEnumerable<ProductQuantity> ParseProducts(IFormFile file)
+ {
+ using StreamReader reader = new(file.OpenReadStream());
+ var config = new CsvConfiguration(CultureInfo.GetCultureInfo("ru-RU"))
+ {
+ HasHeaderRecord = false,
+ };
+ using CsvReader csvReader = new(reader, config);
+
+ return csvReader.GetRecords<ProductQuantity>().ToList();
+ }
+}
diff --git a/RhSolutions.SkuParser.Api/Services/ExcelParser.cs b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs
new file mode 100644
index 0000000..27b10bd
--- /dev/null
+++ b/RhSolutions.SkuParser.Api/Services/ExcelParser.cs
@@ -0,0 +1,76 @@
+using ClosedXML.Excel;
+using RhSolutions.SkuParser.Models;
+
+namespace RhSolutions.SkuParser.Services;
+
+public class ExcelParser : ISkuParser
+{
+ public IEnumerable<ProductQuantity> ParseProducts(IFormFile file)
+ {
+ using XLWorkbook workbook = new(file.OpenReadStream());
+ IXLWorksheet ws = workbook.Worksheet(1);
+
+ var leftTop = ws.FirstCellUsed()?.Address;
+ var rightBottom = ws.LastCellUsed()?.Address;
+ if (new object?[] { leftTop, rightBottom }.Any(x => x == null))
+ {
+ throw new ArgumentException($"Таблица пуста: {file.FileName}");
+ }
+
+ var lookupRange = ws.Range(leftTop, rightBottom).RangeUsed();
+ var columns = lookupRange.Columns();
+
+ var skuColumnQuantity = columns
+ .Select(column => new
+ {
+ Column = column,
+ Products = column.CellsUsed()
+ .Select(cell => !cell.HasFormula && Product.TryParse(cell.Value.ToString(), out Product? p) ? p : null)
+ })
+ .Select(c => new { c.Column, SkuCount = c.Products.Count(p => p != null) })
+ .Aggregate((l, r) => l.SkuCount > r.SkuCount ? l : r);
+ var skuColumn = skuColumnQuantity.SkuCount > 0 ? skuColumnQuantity.Column : null;
+
+ if (skuColumn == null)
+ {
+ throw new ArgumentException($"Столбец с артикулом не определен: {file.FileName}");
+ }
+
+ var quantityColumn = lookupRange.Columns().Skip(skuColumn.ColumnNumber())
+ .Select(column => new
+ {
+ Column = column,
+ IsColumnWithNumbers = column.CellsUsed()
+ .Count(cell => cell.Value.IsNumber == true) > column.CellsUsed().Count() / 4
+ })
+ .First(x => x.IsColumnWithNumbers)
+ .Column;
+
+ if (quantityColumn == null)
+ {
+ throw new ArgumentException($"Столбец с количеством не определен: {file.FileName}");
+ }
+
+ List<ProductQuantity> result = new();
+ var rows = quantityColumn.CellsUsed().Select(x => x.Address.RowNumber);
+
+ foreach (var row in rows)
+ {
+ var quantity = quantityColumn.Cell(row).Value;
+ var sku = skuColumn.Cell(row).Value;
+
+ if (quantity.IsNumber
+ && Product.TryParse(sku.ToString(), out Product? p))
+ {
+ ProductQuantity pq = new()
+ {
+ Product = p!,
+ Quantity = quantity.GetNumber()
+ };
+ result.Add(pq);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/RhSolutions.SkuParser.Api/Services/ISkuParser.cs b/RhSolutions.SkuParser.Api/Services/ISkuParser.cs
new file mode 100644
index 0000000..98b9d9c
--- /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 IEnumerable<ProductQuantity> ParseProducts(IFormFile file);
+}