Implement csv parser
This commit is contained in:
parent
08e86b43c0
commit
30f2e28c87
48
RhSolutions.SkuParser.Api/Controllers/ProductsController.cs
Normal file
48
RhSolutions.SkuParser.Api/Controllers/ProductsController.cs
Normal file
@ -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 async Task<IActionResult> PostFiles()
|
||||||
|
{
|
||||||
|
IFormFileCollection files = Request.Form.Files;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
ISkuParser parser = _provider.GetRequiredKeyedService<ISkuParser>(file.ContentType);
|
||||||
|
IEnumerable<ProductQuantity> 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 }));
|
||||||
|
}
|
||||||
|
}
|
65
RhSolutions.SkuParser.Api/Models/Product.cs
Normal file
65
RhSolutions.SkuParser.Api/Models/Product.cs
Normal file
@ -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;
|
||||||
|
}
|
11
RhSolutions.SkuParser.Api/Models/ProductQuantity.cs
Normal file
11
RhSolutions.SkuParser.Api/Models/ProductQuantity.cs
Normal file
@ -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; }
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
|
using RhSolutions.SkuParser.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Services.AddKeyedScoped<ISkuParser, CsvParser>("text/csv");
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
app.MapControllers();
|
||||||
app.MapGet("/", () => "Hello World!");
|
app.Run();
|
||||||
|
|
||||||
app.Run();
|
|
@ -6,4 +6,9 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ClosedXML" Version="0.102.3" />
|
||||||
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
28
RhSolutions.SkuParser.Api/Services/CsvParser.cs
Normal file
28
RhSolutions.SkuParser.Api/Services/CsvParser.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using CsvHelper;
|
||||||
|
using CsvHelper.Configuration;
|
||||||
|
using RhSolutions.SkuParser.Models;
|
||||||
|
|
||||||
|
namespace RhSolutions.SkuParser.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Парсер артикулов и их количества из файлов *.csv
|
||||||
|
/// </summary>
|
||||||
|
public class CsvParser : ISkuParser
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<ProductQuantity>> 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<ProductQuantity>().ToList();
|
||||||
|
}
|
||||||
|
}
|
11
RhSolutions.SkuParser.Api/Services/ExcelParser.cs
Normal file
11
RhSolutions.SkuParser.Api/Services/ExcelParser.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using RhSolutions.SkuParser.Models;
|
||||||
|
|
||||||
|
namespace RhSolutions.SkuParser.Services;
|
||||||
|
|
||||||
|
public class ExcelParser : ISkuParser
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<ProductQuantity>> ParseProducts(IFormFile file)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
7
RhSolutions.SkuParser.Api/Services/ISkuParser.cs
Normal file
7
RhSolutions.SkuParser.Api/Services/ISkuParser.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using RhSolutions.SkuParser.Models;
|
||||||
|
|
||||||
|
namespace RhSolutions.SkuParser.Services;
|
||||||
|
public interface ISkuParser
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<ProductQuantity>> ParseProducts(IFormFile file);
|
||||||
|
}
|
2
RhSolutions.SkuParser.Tests/GlobalUsings.cs
Normal file
2
RhSolutions.SkuParser.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
global using NUnit.Framework;
|
||||||
|
global using RhSolutions.SkuParser.Models;
|
74
RhSolutions.SkuParser.Tests/ProductTests.cs
Normal file
74
RhSolutions.SkuParser.Tests/ProductTests.cs
Normal file
@ -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<Product> set = new();
|
||||||
|
if (Product.TryParse(value, out var product))
|
||||||
|
{
|
||||||
|
set.Add(product!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Fail($"Parsing failed on {value}");
|
||||||
|
}
|
||||||
|
Assert.True(set.Contains(product!));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\RhSolutions.SkuParser.Api\RhSolutions.SkuParser.Api.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Api", "RhSolutions.SkuParser.Api\RhSolutions.SkuParser.Api.csproj", "{5178E712-F984-48F4-9C68-6DC8CCAA4053}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.SkuParser.Tests", "RhSolutions.SkuParser.Tests\RhSolutions.SkuParser.Tests.csproj", "{8EB147E6-E7B9-42AB-B634-BA2EA1A7A542}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{5178E712-F984-48F4-9C68-6DC8CCAA4053}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Reference in New Issue
Block a user