// Package gtin validates and normalizes GS1 trade item numbers (GTIN-8/12/13/14). // Only globally-unique GS1 codes are accepted: store-internal / variable-weight / // coupon codes (which are not globally unique) are rejected on purpose. package gtin import ( "errors" "strings" ) // Validation errors. var ( ErrEmpty = errors.New("条码不能为空") ErrFormat = errors.New("条码必须为 8/12/13/14 位数字") ErrCheck = errors.New("条码校验位不正确") ErrRestricted = errors.New("店内码/变量重量码/优惠券码等非全球唯一码,不予收录") ) // Normalize trims and validates a GTIN, returning the cleaned digit string. // It enforces length, the GS1 mod-10 check digit, and rejects restricted // (non-globally-unique) number ranges. func Normalize(raw string) (string, error) { s := strings.TrimSpace(raw) if s == "" { return "", ErrEmpty } for _, c := range s { if c < '0' || c > '9' { return "", ErrFormat } } switch len(s) { case 8, 12, 13, 14: default: return "", ErrFormat } if !validCheckDigit(s) { return "", ErrCheck } if restricted(s) { return "", ErrRestricted } return s, nil } // InferType returns the conventional GTIN type label for a normalized code. func InferType(s string) string { switch len(s) { case 8: return "EAN8" case 12: return "UPC" case 14: return "GTIN14" default: return "EAN13" } } // validCheckDigit verifies the trailing GS1 mod-10 check digit. The digit // immediately left of the check digit carries weight 3, then weights alternate. func validCheckDigit(s string) bool { n := len(s) sum := 0 for i := 0; i < n-1; i++ { d := int(s[i] - '0') if (n-1-i)%2 == 1 { sum += d * 3 } else { sum += d } } check := (10 - (sum % 10)) % 10 return check == int(s[n-1]-'0') } // restricted reports whether a (length/check-digit valid) code falls in a // number range reserved for non-globally-unique use. func restricted(s string) bool { switch len(s) { case 13: p2 := s[:2] switch { case s[0] == '2': // 20-29 restricted distribution / in-store return true case p2 == "02": // 020-029 variable-measure within a store return true case p2 == "04": // 040-049 restricted circulation within a company return true case p2 == "05": // 050-059 coupons return true case p2 == "98" || p2 == "99": // 980-989/99 coupons & refund receipts return true } case 12: // UPC-A: leading number-system digit switch s[0] { case '2': // in-store / random weight return true case '4': // unrestricted in-store use return true case '5': // coupons return true } case 8: // EAN-8: 0/2 prefixes reserved for in-store use if s[0] == '0' || s[0] == '2' { return true } } return false }