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);
|
||||
builder.Services.AddKeyedScoped<ISkuParser, CsvParser>("text/csv");
|
||||
builder.Services.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => "Hello World!");
|
||||
|
||||
app.Run();
|
||||
app.MapControllers();
|
||||
app.Run();
|
@ -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>
|
||||
|
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
|
||||
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
|
||||
|
Reference in New Issue
Block a user