phronCare/Transversal/Services/Gs1CodeParser.cs
Leandro Hernan Rojas 2d955752c5
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 3m17s
Update Fix1
2025-08-18 01:15:14 -03:00

180 lines
6.5 KiB
C#

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