1
0

Implement csv parser

This commit is contained in:
Serghei Cebotari 2024-07-20 16:58:35 +03:00
parent 08e86b43c0
commit 30f2e28c87
12 changed files with 288 additions and 4 deletions

View 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 }));
}
}

View 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;
}

View 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; }
}

View File

@ -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();

View File

@ -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>

View 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();
}
}

View 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();
}
}

View File

@ -0,0 +1,7 @@
using RhSolutions.SkuParser.Models;
namespace RhSolutions.SkuParser.Services;
public interface ISkuParser
{
public Task<IEnumerable<ProductQuantity>> ParseProducts(IFormFile file);
}

View File

@ -0,0 +1,2 @@
global using NUnit.Framework;
global using RhSolutions.SkuParser.Models;

View 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!));
}
}

View File

@ -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>

View File

@ -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