using Domain.Dtos.Stock; namespace Transversal.Services { public static class Gs1CodeParser { // GS1 FNC1 (ASCII 29) private const char FNC1 = (char)29; /// /// 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). /// 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; } /// /// 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". /// 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); } } }