phronCare/Transversal/Services/Gs1CodeParser.cs
Leandro Hernan Rojas 1c4c241266
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 15m47s
Add StockItemModal v1
2025-08-18 00:47:37 -03:00

182 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Domain.Dtos.Stock;
namespace Transversal.Services
{
public static class Gs1CodeParser
{
// GS1 FNC1 (ASCII 29)
private const char FNC1 = (char)29;
/// <summary>
/// Parsea un string GS1 (GS1-128/EAN-128/DataMatrix) con AIs en cualquier orden.
/// Soporta separadores FNC1 (ASCII 29), '$' y espacios. AIs: (01),(10),(11),(17),(21),(22).
/// </summary>
public static Gs1ScanResult Parse(string raw)
{
var result = new Gs1ScanResult
{
Raw = raw?.Trim() ?? string.Empty
};
if (string.IsNullOrWhiteSpace(raw))
return result;
// 1) Normalizar: $ y espacios -> FNC1; quitar saltos
var s = raw.Trim()
.Replace("\r", "")
.Replace("\n", "")
.Replace("$", FNC1.ToString());
// algunos escáneres meten espacios sueltos entre AIs/valores
while (s.Contains(" ")) s = s.Replace(" ", " ");
s = s.Replace(" ", string.Empty);
int i = 0;
while (i < s.Length)
{
if (s[i] == FNC1) { i++; continue; }
if (i + 2 > s.Length) break;
// Intentar leer AI de dos dígitos (los que usamos acá)
var ai = s.Substring(i, 2);
i += 2;
switch (ai)
{
case "01": // GTIN (14 fijos)
if (TryTakeFixed(s, ref i, 14, out var gtin))
result.Gtin = gtin;
break;
case "17": // Expiración YYMMDD (6 fijos)
if (TryTakeFixed(s, ref i, 6, out var yymmdd)
&& TryParseYyMmDdToDateTime(yymmdd, out var expDt))
{
result.ExpirationDate = expDt;
}
break;
case "11": // Fabricación YYMMDD (6 fijos) -> opcional guardar
if (TryTakeFixed(s, ref i, 6, out var mfgYymmdd)
&& TryParseYyMmDdToDateTime(mfgYymmdd, out var mfgDt))
{
// Si querés guardar: agregá ManufacturingDate en tu DTO
// result.ManufacturingDate = mfgDt;
}
break;
case "10": // Lote (var-length)
result.Lot = TakeVariable(s, ref i);
break;
case "21": // Serie (var-length)
result.Serial = TakeVariable(s, ref i);
break;
case "22": // Variante (var-length)
result.Variant = TakeVariable(s, ref i);
break;
default:
// Heurística: si el char previo a este AI no era FNC1, podría ser que no era AI real.
// Retrocede 1 y avanza de a 1 hasta próximo FNC1 o final.
i -= 1;
SkipUnknownUntilFnc1(s, ref i);
break;
}
}
// Heurística: 17 seguido de 10 sin FNC1 (p.ej. ...17YYMMDD10LOTE)
if (result.ExpirationDate.HasValue && string.IsNullOrEmpty(result.Lot))
{
var idx17 = s.IndexOf("17", StringComparison.Ordinal);
if (idx17 >= 0 && idx17 + 8 < s.Length) // "17" + 6 chars de fecha = +8
{
var after17 = s.Substring(idx17 + 8);
var idx10 = after17.IndexOf("10", StringComparison.Ordinal);
if (idx10 >= 0)
{
var start = idx17 + 8 + idx10 + 2;
var lot = ReadUntilFnc1OrEnd(s, start);
if (!string.IsNullOrEmpty(lot))
result.Lot = lot;
}
}
}
return result;
}
// === Helpers ===
private static bool TryTakeFixed(string s, ref int i, int length, out string value)
{
value = string.Empty;
if (i + length > s.Length) return false;
value = s.Substring(i, length);
i += length;
return true;
}
/// <summary>
/// Extrae un valor de longitud variable (por ejemplo, Batch o Serial) del código GS1.
/// Cortamos únicamente cuando encontramos un separador FNC1.
/// No se intenta detectar un posible AI dentro del valor, ya que hay casos donde el valor
/// legítimamente contiene secuencias como "22" o "17" que podrían confundirse con un AI.
/// Ejemplo: un lote "52360227" no debe cortarse en "52360" al detectar "22".
/// </summary>
private static string TakeVariable(string s, ref int i)
{
int start = i;
while (i < s.Length && s[i] != FNC1) i++;
return s.Substring(start, i - start);
}
private static bool LooksLikeNextAi(string s, int index)
{
if (index + 1 >= s.Length) return false;
// AIs que nos interesan acá arrancan con 02 y son de 2 dígitos (01,10,11,17,21,22)
return char.IsDigit(s[index]) && char.IsDigit(s[index + 1]) &&
(s[index] == '0' || s[index] == '1' || s[index] == '2') &&
(s.Substring(index, 2) is "01" or "10" or "11" or "17" or "21" or "22");
}
private static void SkipUnknownUntilFnc1(string s, ref int i)
{
while (i < s.Length && s[i] != FNC1) i++;
if (i < s.Length && s[i] == FNC1) i++; // consumir FNC1 si lo hay
}
private static bool TryParseYyMmDdToDateTime(string yymmdd, out DateTime dt)
{
dt = default;
if (yymmdd?.Length != 6) return false;
int yy = int.Parse(yymmdd.Substring(0, 2));
int mm = int.Parse(yymmdd.Substring(2, 2));
int dd = int.Parse(yymmdd.Substring(4, 2));
// GS1 usa 0099; se suele mapear a 20002099. Ajustá si necesitás 19901999 para 9099.
int year = 2000 + yy;
try
{
dt = new DateTime(year, mm, dd);
return true;
}
catch
{
return false;
}
}
private static string ReadUntilFnc1OrEnd(string s, int start)
{
int i = start;
while (i < s.Length && s[i] != FNC1) i++;
return s.Substring(start, i - start);
}
}
}