GS1 barcode data
GS1 data for product and pallet labels: element strings read from a scanner and written for GS1-128 or DataMatrix, each AI checked with its dates and weights, SSCC numbers, GTIN check digits, GS1 Digital Link addresses, and a logistic label's content.
Version 1.0.0 · 9 functions · 0 crates
Functions
What use gs1_labels.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use gs1_parse, gs1_build from gs1_labels (Quillo 0.6).
gs1_parse(data: str) → anyor an errorElement data read and each AI checked: bracketed (01)09501101530003(17)270131(10)AB12, raw with GS separators (a scanner's output, ]C1, ]d2 or ]Q3 first), or | in place of GS. Gives { ok, elements: [{ ai, title, value, ok, error, date (YYYY-MM-DD for dates), number and unit (310n weights...), gtin13 }], errors }.
gs1_build(elements: any) → anyor an errorElement data written from { "01": "09501101530003", "10": "AB12" } (or a list of [ai, value]): each checked, fixed-length AIs first as GS1 recommends. Gives { brackets: "(01)…(10)…", raw: with GS (\u001d) after variable fields, for a 2D code's data, hri: "(01) 09501101530003 (10) AB12" }; an AI that fails its check is an error.
gs1_check_digit(digits: str) → intor an errorThe GS1 check digit for a code without it (GTIN-8/12/13/14, SSCC, GLN, GSIN).
gs1_valid(code: str) → boolWhether a GTIN (8, 12, 13 or 14 digits), GLN (13) or SSCC (18) has the right check digit.
gs1_gtin14(gtin: str) → stror an errorA GTIN of any length as GTIN-14 (zeros in front), checked.
gs1_sscc(extension: int, company_prefix: str, serial: int) → stror an errorAn SSCC (Serial Shipping Container Code) made: the extension digit (0-9), the company prefix and the serial reference, padded to 17 digits and the check digit added.
gs1_digital_link(elements: any, domain: str) → stror an errorA GS1 Digital Link web address for the elements: the primary key (GTIN, SSCC, GLN...) and its qualifiers (CPV, lot, serial) in the path, the rest as query parameters, on domain (https://id.gs1.org when empty).
gs1_digital_link_parse(url: str) → anyor an errorA GS1 Digital Link address read back into its elements, checked: { ok, domain, elements: [...] } as gs1_parse gives them. Short names like /gtin/ are taken too.
gs1_logistic_label(data: any) → anyor an errorThe content of a GS1 logistic label (the SSCC-18 pallet or carton label): data is { from: [lines], to: [lines], sscc, content (GTIN of what is inside), count, batch, best_before or use_by (YYYY-MM-DD), net_kg, order, ship_to_post }. Gives the three sections (supplier, carrier, customer... as text lines) and the barcodes to print, as bracketed element strings: the content barcode, and the SSCC last, at the bottom, as GS1 asks.
Get it
$ quillo install gs1_labels # gs1_labels.qwer into this folder$ quillo install gs1_labels --example # and its example, gs1_labels.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/gs1_labels.qwer$ curl -O https://quillolang.org/plugins/files/gs1_labels.qweThen, in your program, all of it or only what you call:
use gs1_labels.qweruse gs1_parse, gs1_build from gs1_labels // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A distributor's GS1 data: what a scanner read from a carton's GS1-128 or DataMatrix, checked// AI by AI; the element strings for new labels; the pallet's SSCC; GS1 Digital Link addresses// for a QR code on the pack; and the content of the pallet's logistic label.//// quillo run gs1_labels.qwe then open http://localhost:8080/scan and http://localhost:8080/palletuse "gs1_labels.qwer"// A scanner's output: ]d2 (GS1 DataMatrix), then the data with GS after the variable-length lot.route GET /scan = gs1_parse("]d20109501101530003101234AB\u{1d}17270131310200125021SN-0042")?route GET /build = { label: gs1_build({ "01": "09501101530003", "10": "LOT-77", "15": "270430", "3103": "001250" })?, bad_check: gs1_build({ "01": "09501101530004" }).error, gtin14: gs1_gtin14("4006381333931")?, valid: [gs1_valid("4006381333931"), gs1_valid("4006381333932")],}route GET /link { let url = gs1_digital_link({ "01": "09501101530003", "10": "AB/12", "17": "270131", "21": "S42" }, "https://brand.example")? { url: url, back: gs1_digital_link_parse(url)?.elements | map((e) => `${e.ai}=${e.value}`) }}route GET /pallet { let sscc = gs1_sscc(3, "0950110", 4471)? gs1_logistic_label({ from: ["Quillo Coffee Roasters", "12 Dock Road, Leeds LS1 4AB"], to: ["Café Montmartre", "75018 Paris"], sscc: sscc, content: "09501101530003", count: 48, batch: "L2610B", best_before: "2027-04-30", net_kg: 96.5, order: "PO-7731", ship_to_post: "75018", })?}$ quillo run gs1_labels.qwe$ curl 'localhost:8080/scan'$ curl 'localhost:8080/build'$ curl 'localhost:8080/link'$ curl 'localhost:8080/pallet'Source
//! GS1 data for barcodes and labels: element strings read and written (bracketed, with FNC1/GS separators, from a scanner's ]C1 ]d2 ]Q3), each application identifier checked against its format with titles, dates and decimal weights read, check digits for GTIN, SSCC and GLN, SSCC numbers made, GS1 Digital Link web addresses built and read, and the content of a GS1 logistic (SSCC-18) label laid out.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Map, Value};/// An AI: its code, title, format (`n` digits, `an` text; fixed or up to), a check digit at the end, a date, decimals in the last digit of the AI.struct Ai { code: &'static str, title: &'static str, kind: char, min: usize, max: usize, check: bool, date: bool,}const fn ai(code: &'static str, title: &'static str, kind: char, min: usize, max: usize, check: bool, date: bool) -> Ai { Ai { code, title, kind, min, max, check, date }}const AIS: &[Ai] = &[ ai("00", "SSCC", 'n', 18, 18, true, false), ai("01", "GTIN", 'n', 14, 14, true, false), ai("02", "CONTENT", 'n', 14, 14, true, false), ai("10", "BATCH/LOT", 'a', 1, 20, false, false), ai("11", "PROD DATE", 'n', 6, 6, false, true), ai("12", "DUE DATE", 'n', 6, 6, false, true), ai("13", "PACK DATE", 'n', 6, 6, false, true), ai("15", "BEST BEFORE or BEST BY", 'n', 6, 6, false, true), ai("16", "SELL BY", 'n', 6, 6, false, true), ai("17", "USE BY or EXPIRY", 'n', 6, 6, false, true), ai("20", "VARIANT", 'n', 2, 2, false, false), ai("21", "SERIAL", 'a', 1, 20, false, false), ai("22", "CPV", 'a', 1, 20, false, false), ai("240", "ADDITIONAL ID", 'a', 1, 30, false, false), ai("241", "CUST. PART No.", 'a', 1, 30, false, false), ai("250", "SECONDARY SERIAL", 'a', 1, 30, false, false), ai("251", "REF. TO SOURCE", 'a', 1, 30, false, false), ai("254", "GLN EXTENSION COMPONENT", 'a', 1, 20, false, false), ai("30", "VAR. COUNT", 'n', 1, 8, false, false), ai("37", "COUNT", 'n', 1, 8, false, false), ai("400", "ORDER NUMBER", 'a', 1, 30, false, false), ai("401", "GINC", 'a', 1, 30, false, false), ai("402", "GSIN", 'n', 17, 17, true, false), ai("403", "ROUTE", 'a', 1, 30, false, false), ai("410", "SHIP TO LOC", 'n', 13, 13, true, false), ai("411", "BILL TO", 'n', 13, 13, true, false), ai("412", "PURCHASE FROM", 'n', 13, 13, true, false), ai("413", "SHIP FOR LOC", 'n', 13, 13, true, false), ai("414", "LOC No.", 'n', 13, 13, true, false), ai("415", "PAY TO", 'n', 13, 13, true, false), ai("416", "PROD/SERV LOC", 'n', 13, 13, true, false), ai("420", "SHIP TO POST", 'a', 1, 20, false, false), ai("422", "ORIGIN", 'n', 3, 3, false, false), ai("7003", "EXPIRY TIME", 'n', 10, 10, false, false), ai("8004", "GIAI", 'a', 1, 30, false, false), ai("8018", "GSRN - PROVIDER", 'n', 18, 18, true, false), ai("8020", "REF No.", 'a', 1, 25, false, false), ai("90", "INTERNAL", 'a', 1, 30, false, false),];/// Measures with a decimal point in the AI's last digit (310n net weight in kg, and so on): (first three digits, title, unit).const MEASURES: &[(&str, &str, &str)] = &[ ("310", "NET WEIGHT (kg)", "kg"), ("311", "LENGTH (m)", "m"), ("312", "WIDTH (m)", "m"), ("313", "HEIGHT (m)", "m"), ("314", "AREA (m²)", "m2"), ("315", "NET VOLUME (l)", "l"), ("316", "NET VOLUME (m³)", "m3"), ("320", "NET WEIGHT (lb)", "lb"), ("330", "GROSS WEIGHT (kg)", "kg"), ("331", "LENGTH (m), log", "m"), ("332", "WIDTH (m), log", "m"), ("333", "HEIGHT (m), log", "m"), ("335", "VOLUME (l), log", "l"), ("336", "VOLUME (m³), log", "m3"), ("340", "GROSS WEIGHT (lb)", "lb"),];/// AIs whose data has a fixed length by their first two digits (no FNC1 after them).fn predefined(code: &str) -> Option<usize> { Some(match &code[..2] { "00" => 18, "01" | "02" | "03" => 14, "04" => 16, "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" => 6, "20" => 2, "31" | "32" | "33" | "34" | "35" | "36" => 6, "41" => 13, _ => return None, })}enum Def { Plain(&'static Ai), Measure(&'static str, &'static str, u32), Internal(String),}fn find(code: &str) -> Option<Def> { if let Some(a) = AIS.iter().find(|a| a.code == code) { return Some(Def::Plain(a)); } if code.len() == 4 { if let Some(m) = MEASURES.iter().find(|m| code.starts_with(m.0)) { let d = code.as_bytes()[3] - b'0'; if d <= 5 { return Some(Def::Measure(m.1, m.2, d as u32)); } } if code.starts_with("392") || code.starts_with("390") { return Some(Def::Measure(if code.starts_with("392") { "PRICE" } else { "AMOUNT" }, "", (code.as_bytes()[3] - b'0') as u32)); } } if code.len() == 2 && ("91".."99").contains(&code) || code == "99" { return Some(Def::Internal(code.to_string())); } None}/// The GS1 check digit for the digits before it (mod 10, weights 3 and 1 from the right).fn check_digit(digits: &str) -> u32 { let s: u32 = digits.chars().rev().enumerate().map(|(i, c)| c.to_digit(10).unwrap_or(0) * if i % 2 == 0 { 3 } else { 1 }).sum(); (10 - s % 10) % 10}/// GS1's character set 82: letters, digits and !"%&'()*+,-./:;<=>?_fn cset82(s: &str) -> bool { s.chars().all(|c| c.is_ascii_alphanumeric() || "!\"%&'()*+,-./:;<=>?_".contains(c))}fn date_of(v: &str) -> Value { let (yy, mm, dd): (i32, u32, u32) = (v[0..2].parse().unwrap_or(0), v[2..4].parse().unwrap_or(0), v[4..6].parse().unwrap_or(0)); // GS1's sliding century: years more than 49 ahead are taken as last century (as of 2026: 76 and later). let this = 2026; let mut year = 2000 + yy; if year - this > 50 { year -= 100; } else if this - year >= 50 { year += 100; } if mm == 0 || mm > 12 || dd > 31 { return Value::Null; } if dd == 0 { // Day 00: the last day of the month. let last = match mm { 2 if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 => 29, 2 => 28, 4 | 6 | 9 | 11 => 30, _ => 31, }; return json!(format!("{:04}-{:02}-{:02}", year, mm, last)); } json!(format!("{:04}-{:02}-{:02}", year, mm, dd))}/// One element checked and described: `{ ai, title, value, ok, error, date | number | unit }`.fn element(code: &str, value: &str) -> Value { let mut e = Map::new(); e.insert("ai".into(), json!(code)); e.insert("value".into(), json!(value)); let mut err: Option<String> = None; match find(code) { None => { e.insert("title".into(), Value::Null); err = Some(format!("({}) is not an AI this plugin knows", code)); } Some(Def::Plain(a)) => { e.insert("title".into(), json!(a.title)); let n = value.chars().count(); if n < a.min || n > a.max { err = Some(if a.min == a.max { format!("({}) takes {} characters, not {}", code, a.min, n) } else { format!("({}) takes up to {} characters, not {}", code, a.max, n) }); } else if a.kind == 'n' && !value.chars().all(|c| c.is_ascii_digit()) { err = Some(format!("({}) takes digits only", code)); } else if a.kind == 'a' && !cset82(value) { err = Some(format!("({}) {:?} has characters GS1 does not allow", code, value)); } else if a.check && check_digit(&value[..n - 1]).to_string() != value[n - 1..] { err = Some(format!("({}) check digit should be {}", code, check_digit(&value[..n - 1]))); } else if a.date { let d = date_of(value); if d.is_null() { err = Some(format!("({}) {} is not a date (YYMMDD)", code, value)); } e.insert("date".into(), d); } if code == "01" || code == "02" { e.insert("gtin13".into(), if value.starts_with('0') { json!(&value[1..]) } else { Value::Null }); } } Some(Def::Measure(title, unit, decimals)) => { e.insert("title".into(), json!(title)); if value.len() != 6 && !title.starts_with("PRICE") && !title.starts_with("AMOUNT") || !value.chars().all(|c| c.is_ascii_digit()) { err = Some(format!("({}) takes 6 digits", code)); } else { let n: f64 = value.parse::<f64>().unwrap_or(0.0) / 10f64.powi(decimals as i32); e.insert("number".into(), json!(n)); if !unit.is_empty() { e.insert("unit".into(), json!(unit)); } } } Some(Def::Internal(_)) => { e.insert("title".into(), json!("INTERNAL")); if value.is_empty() || value.len() > 90 || !cset82(value) { err = Some(format!("({}) takes 1 to 90 characters of GS1's set", code)); } } } e.insert("ok".into(), json!(err.is_none())); e.insert("error".into(), json!(err)); Value::Object(e)}/// Splits element data into (AI, value) pairs: bracketed `(01)...(10)...`, or raw with GS (or `|`) after variable-length fields, a symbology prefix taken off.fn split(s: &str) -> Result<Vec<(String, String)>, String> { let s = s.trim(); let s = ["]C1", "]e0", "]d2", "]Q3", "]J1"].iter().find_map(|p| s.strip_prefix(p)).unwrap_or(s); let mut out = vec![]; if s.starts_with('(') { let mut rest = s; while let Some(r) = rest.strip_prefix('(') { let end = r.find(')').ok_or("a ( without its )")?; let code = &r[..end]; let r = &r[end + 1..]; let next = r.find('(').unwrap_or(r.len()); out.push((code.to_string(), r[..next].to_string())); rest = &r[next..]; } if !rest.is_empty() { return Err(format!("unexpected {:?}", rest)); } return Ok(out); } let cs: Vec<char> = s.chars().collect(); let mut i = 0; while i < cs.len() { if cs[i] == '\u{1d}' || cs[i] == '|' { i += 1; continue; } // The AI: the shortest prefix this plugin knows (2, 3 or 4 digits). let mut code = None; for n in 2..=4 { if i + n <= cs.len() { let c: String = cs[i..i + n].iter().collect(); if c.chars().all(|c| c.is_ascii_digit()) && find(&c).is_some() { code = Some(c); break; } } } let code = code.ok_or_else(|| format!("no known AI at {:?}", cs[i..].iter().take(6).collect::<String>()))?; i += code.len(); let len = match predefined(&code) { Some(l) => l, None => cs[i..].iter().position(|c| *c == '\u{1d}' || *c == '|').unwrap_or(cs.len() - i), }; if i + len > cs.len() { return Err(format!("({}) is cut short", code)); } out.push((code, cs[i..i + len].iter().collect())); i += len; } Ok(out)}/// Element data read and each AI checked: bracketed `(01)09501101530003(17)270131(10)AB12`, raw with GS separators (a scanner's output, `]C1`, `]d2` or `]Q3` first), or `|` in place of GS./// Gives `{ ok, elements: [{ ai, title, value, ok, error, date (YYYY-MM-DD for dates), number and unit (310n weights...), gtin13 }], errors }`.pub fn gs1_parse(data: &str) -> Result<serde_json::Value, String> { let pairs = split(data)?; let elements: Vec<Value> = pairs.iter().map(|(a, v)| element(a, v)).collect(); let errors: Vec<Value> = elements.iter().filter_map(|e| e["error"].as_str().map(|s| json!(s))).collect(); Ok(json!({ "ok": errors.is_empty(), "elements": elements, "errors": errors }))}fn pairs_of(elements: &Value) -> Result<Vec<(String, String)>, String> { match elements { Value::Object(m) => Ok(m.iter().map(|(k, v)| (k.clone(), match v { Value::String(s) => s.clone(), o => o.to_string() })).collect()), Value::Array(a) => a .iter() .map(|p| match p.as_array() { Some(x) if x.len() == 2 => Ok((x[0].as_str().unwrap_or("").to_string(), match &x[1] { Value::String(s) => s.clone(), o => o.to_string() })), _ => Err("each element is [ai, value]".to_string()), }) .collect(), Value::String(s) => split(s), _ => Err("elements are a map { ai: value }, a list of [ai, value], or element text".into()), }}/// Element data written from `{ "01": "09501101530003", "10": "AB12" }` (or a list of [ai, value]): each checked, fixed-length AIs first as GS1 recommends./// Gives `{ brackets: "(01)…(10)…", raw: with GS (\u001d) after variable fields, for a 2D code's data, hri: "(01) 09501101530003 (10) AB12" }`; an AI that fails its check is an error.pub fn gs1_build(elements: serde_json::Value) -> Result<serde_json::Value, String> { let mut pairs = pairs_of(&elements)?; for (a, v) in &pairs { let e = element(a, v); if let Some(err) = e["error"].as_str() { return Err(err.to_string()); } } pairs.sort_by_key(|(a, _)| if predefined(a).is_some() { 0 } else { 1 }); let brackets: String = pairs.iter().map(|(a, v)| format!("({}){}", a, v)).collect(); let mut raw = String::new(); for (k, (a, v)) in pairs.iter().enumerate() { raw.push_str(a); raw.push_str(v); if predefined(a).is_none() && k + 1 < pairs.len() { raw.push('\u{1d}'); } } let hri: Vec<String> = pairs.iter().map(|(a, v)| format!("({}) {}", a, v)).collect(); Ok(json!({ "brackets": brackets, "raw": raw, "hri": hri.join(" ") }))}/// The GS1 check digit for a code without it (GTIN-8/12/13/14, SSCC, GLN, GSIN).pub fn gs1_check_digit(digits: &str) -> Result<i64, String> { if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { return Err("digits only".into()); } Ok(check_digit(digits) as i64)}/// Whether a GTIN (8, 12, 13 or 14 digits), GLN (13) or SSCC (18) has the right check digit.pub fn gs1_valid(code: &str) -> bool { let n = code.len(); [8, 12, 13, 14, 17, 18].contains(&n) && code.chars().all(|c| c.is_ascii_digit()) && check_digit(&code[..n - 1]).to_string() == code[n - 1..]}/// A GTIN of any length as GTIN-14 (zeros in front), checked.pub fn gs1_gtin14(gtin: &str) -> Result<String, String> { let g: String = gtin.chars().filter(|c| !c.is_whitespace() && *c != '-').collect(); if !gs1_valid(&g) || g.len() > 14 { return Err(format!("{:?} is not a GTIN with a right check digit", gtin)); } Ok(format!("{:0>14}", g))}/// An SSCC (Serial Shipping Container Code) made: the extension digit (0-9), the company prefix and the serial reference, padded to 17 digits and the check digit added.pub fn gs1_sscc(extension: i64, company_prefix: &str, serial: i64) -> Result<String, String> { if !(0..=9).contains(&extension) { return Err("the extension digit is 0 to 9".into()); } if company_prefix.len() < 6 || company_prefix.len() > 12 || !company_prefix.chars().all(|c| c.is_ascii_digit()) { return Err("a GS1 company prefix is 6 to 12 digits".into()); } let room = 16 - company_prefix.len(); let s = serial.to_string(); if serial < 0 || s.len() > room { return Err(format!("with a {}-digit prefix the serial reference has {} digits at most", company_prefix.len(), room)); } let body = format!("{}{}{:0>w$}", extension, company_prefix, s, w = room); Ok(format!("{}{}", body, check_digit(&body)))}fn pct(s: &str) -> String { let mut out = String::new(); for b in s.bytes() { if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { out.push(b as char); } else { out.push_str(&format!("%{:02X}", b)); } } out}fn unpct(s: &str) -> String { let b = s.as_bytes(); let mut out = vec![]; let mut i = 0; while i < b.len() { if b[i] == b'%' && i + 2 < b.len() { if let Ok(x) = u8::from_str_radix(&s[i + 1..i + 3], 16) { out.push(x); i += 3; continue; } } out.push(if b[i] == b'+' { b' ' } else { b[i] }); i += 1; } String::from_utf8_lossy(&out).into_owned()}/// Primary keys of Digital Link and their qualifiers, in the order the path takes them.const KEYS: &[(&str, &[&str])] = &[("01", &["22", "10", "21"]), ("00", &[]), ("414", &["254"]), ("8004", &[]), ("8018", &[]), ("402", &[]), ("410", &[]), ("415", &[])];/// A GS1 Digital Link web address for the elements: the primary key (GTIN, SSCC, GLN...) and its qualifiers (CPV, lot, serial) in the path, the rest as query parameters, on `domain` (`https://id.gs1.org` when empty).pub fn gs1_digital_link(elements: serde_json::Value, domain: &str) -> Result<String, String> { let pairs = pairs_of(&elements)?; for (a, v) in &pairs { if let Some(err) = element(a, v)["error"].as_str() { return Err(err.to_string()); } } let (key, quals) = KEYS.iter().find(|(k, _)| pairs.iter().any(|(a, _)| a == k)).ok_or("a Digital Link needs a primary key: (01) GTIN, (00) SSCC, (414) GLN, (8004) GIAI...")?; let get = |a: &str| pairs.iter().find(|(x, _)| x == a).map(|(_, v)| v.clone()); let base = if domain.trim().is_empty() { "https://id.gs1.org".to_string() } else { domain.trim_end_matches('/').to_string() }; let mut url = format!("{}/{}/{}", base, key, pct(&get(key).unwrap())); for q in quals.iter() { if let Some(v) = get(q) { url.push_str(&format!("/{}/{}", q, pct(&v))); } } let rest: Vec<String> = pairs.iter().filter(|(a, _)| a != key && !quals.contains(&a.as_str())).map(|(a, v)| format!("{}={}", a, pct(v))).collect(); if !rest.is_empty() { url.push('?'); url.push_str(&rest.join("&")); } Ok(url)}/// A GS1 Digital Link address read back into its elements, checked: `{ ok, domain, elements: [...] }` as `gs1_parse` gives them. Short names like /gtin/ are taken too.pub fn gs1_digital_link_parse(url: &str) -> Result<serde_json::Value, String> { let (_, rest) = url.split_once("://").ok_or("not a web address")?; let (host, path_q) = rest.split_at(rest.find('/').unwrap_or(rest.len())); let (path, query) = path_q.split_once('?').unwrap_or((path_q, "")); let names: &[(&str, &str)] = &[("gtin", "01"), ("sscc", "00"), ("gln", "414"), ("lot", "10"), ("ser", "21"), ("cpv", "22"), ("giai", "8004"), ("exp", "17")]; let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); // The key is the last run of AI/value pairs: find where a known primary key starts. let start = segs.iter().position(|s| KEYS.iter().any(|(k, _)| k == s) || names.iter().any(|(n, k)| n == s && KEYS.iter().any(|(kk, _)| kk == k))).ok_or("no primary key in the path")?; let mut pairs = vec![]; let mut i = start; while i + 1 < segs.len() { let a = names.iter().find(|(n, _)| *n == segs[i]).map(|(_, k)| k.to_string()).unwrap_or(segs[i].to_string()); pairs.push((a, unpct(segs[i + 1]))); i += 2; } for kv in query.split('&').filter(|s| !s.is_empty()) { let (k, v) = kv.split_once('=').unwrap_or((kv, "")); if k.chars().all(|c| c.is_ascii_digit()) { pairs.push((k.to_string(), unpct(v))); } } let elements: Vec<Value> = pairs.iter().map(|(a, v)| element(a, v)).collect(); let ok = elements.iter().all(|e| e["ok"] == true); let prefix = segs[..start].join("/"); Ok(json!({ "ok": ok, "domain": format!("{}{}", host, if prefix.is_empty() { String::new() } else { format!("/{}", prefix) }), "elements": elements }))}/// The content of a GS1 logistic label (the SSCC-18 pallet or carton label): `data` is `{ from: [lines], to: [lines], sscc, content (GTIN of what is inside), count, batch, best_before or use_by (YYYY-MM-DD), net_kg, order, ship_to_post }`./// Gives the three sections (supplier, carrier, customer... as text lines) and the barcodes to print, as bracketed element strings: the content barcode, and the SSCC last, at the bottom, as GS1 asks.pub fn gs1_logistic_label(data: serde_json::Value) -> Result<serde_json::Value, String> { let s = |k: &str| data[k].as_str().map(str::to_string); let sscc = s("sscc").ok_or("a logistic label needs its `sscc`")?; let sscc = if sscc.len() == 17 { format!("{}{}", sscc, check_digit(&sscc)) } else { sscc }; let ymd = |d: &str| -> Result<String, String> { let p: Vec<&str> = d.split('-').collect(); if p.len() != 3 || p[0].len() != 4 { return Err(format!("{:?} is not a date (YYYY-MM-DD)", d)); } Ok(format!("{}{}{}", &p[0][2..], p[1], p[2])) }; let mut content: Vec<(String, String)> = vec![]; if let Some(g) = s("content") { content.push(("02".into(), gs1_gtin14(&g)?)); } if let Some(c) = data["count"].as_i64() { content.push(("37".into(), c.to_string())); } if let Some(d) = s("best_before") { content.push(("15".into(), ymd(&d)?)); } if let Some(d) = s("use_by") { content.push(("17".into(), ymd(&d)?)); } if let Some(kg) = data["net_kg"].as_f64() { content.push(("3102".into(), format!("{:06}", (kg * 100.0).round() as i64))); } if let Some(b) = s("batch") { content.push(("10".into(), b)); } let mut ship: Vec<(String, String)> = vec![]; if let Some(o) = s("order") { ship.push(("400".into(), o)); } if let Some(p) = s("ship_to_post") { ship.push(("420".into(), p)); } let to_val = |v: &[(String, String)]| Value::Array(v.iter().map(|(a, x)| json!([a, x])).collect()); let mut barcodes = vec![]; let mut lines = vec![]; for group in [&content, &ship] { if !group.is_empty() { let b = gs1_build(to_val(group))?; barcodes.push(b["brackets"].clone()); for (a, v) in group.iter() { let e = element(a, v); let shown = e["date"].as_str().map(str::to_string).or_else(|| e["number"].as_f64().map(|n| format!("{} {}", n, e["unit"].as_str().unwrap_or("")))).unwrap_or(v.clone()); lines.push(json!(format!("{}: {}", e["title"].as_str().unwrap_or(a), shown.trim()))); } } } let s_el = element("00", &sscc); if let Some(err) = s_el["error"].as_str() { return Err(err.to_string()); } barcodes.push(json!(format!("(00){}", sscc))); let list = |k: &str| data[k].as_array().cloned().unwrap_or_default(); Ok(json!({ "supplier": { "from": list("from") }, "customer": { "to": list("to"), "lines": lines }, "sscc": sscc, "sscc_text": format!("SSCC {} {} {} {}", &sscc[..1], &sscc[1..8], &sscc[8..17], &sscc[17..]), "barcodes": barcodes, }))}Related
tspl.qwer · 1.0.0TSC TSPL labels for TSC, Xprinter, Rollo and Munbyn printers: UTF-8 text and blocks, Code 128, GS1-128, EAN-13, ITF-14, QR and Data Matrix, boxes and pictures, and the printer's status before a job goes to port 9100.7 views · 7 downloadsZebra ZPL labels zpl.qwer · 1.0.0Zebra ZPL II labels from fields in millimetres: UTF-8 text, Code 128, GS1-128, QR, Data Matrix and other barcodes, boxes, images as compressed graphics, serial numbers and printer settings, sent to port 9100.8 views · 9 downloadsCPCL mobile printers cpcl.qwer · 1.0.0Zebra CPCL for mobile printers on vans and shop floors: labels and receipts with magnified text, centred lines, Code 128, EAN-13 and QR codes with their text, boxes, inverse lines and pictures, sent over Wi-Fi.7 views · 7 downloadsDatamax DPL labels dpl.qwer · 1.0.0Honeywell and Datamax-O'Neil DPL labels from fields placed in millimetres from the top left: resident fonts, Code 128, EAN-13, QR and Data Matrix, lines and boxes, heat, speed and quantity, sent to port 9100.7 views · 10 downloadsEPL2 labels epl.qwer · 1.0.0Zebra and Eltron EPL2 labels from fields in millimetres: text in the resident fonts, Code 128, EAN-13, Code 39 and QR barcodes, lines, boxes and pictures, with label size, gap, speed and copies, sent to port 9100.6 views · 6 downloadsGodex EZPL labels ezpl.qwer · 1.0.0Godex EZPL labels for G500, RT200 and other Godex printers: text in the resident fonts, Code 128, EAN-13, Code 39 and QR codes, lines and boxes, label size, darkness, speed and copies, sent to port 9100.6 views · 8 downloadsThread 0 posts
On GS1 barcode data: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.