gtin14.patch
text/plain
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
| File | + | − |
|---|---|---|
| expected/isn.out | 75 | 1 |
| isn--1.1.sql | 153 | 0 |
| isn.c | 258 | 7 |
| sql/isn.sql | 29 | 0 |
diff --git a/expected/isn.out b/expected/isn.out
index 18fe37a..b313641 100644
--- a/expected/isn.out
+++ b/expected/isn.out
@@ -18,6 +18,8 @@ INFO: operator family "isn_ops" of access method btree is missing cross-type op
INFO: operator family "isn_ops" of access method btree is missing cross-type operator(s)
INFO: operator family "isn_ops" of access method btree is missing cross-type operator(s)
INFO: operator family "isn_ops" of access method btree is missing cross-type operator(s)
+INFO: operator family "isn_ops" of access method btree is missing cross-type operator(s)
+INFO: operator family "isn_ops" of access method hash is missing cross-type operator(s)
INFO: operator family "isn_ops" of access method hash is missing cross-type operator(s)
INFO: operator family "isn_ops" of access method hash is missing cross-type operator(s)
INFO: operator family "isn_ops" of access method hash is missing cross-type operator(s)
@@ -29,6 +31,7 @@ INFO: operator family "isn_ops" of access method hash is missing cross-type ope
amname | opcname
--------+------------
btree | ean13_ops
+ btree | gtin14_ops
btree | isbn13_ops
btree | isbn_ops
btree | ismn13_ops
@@ -37,6 +40,7 @@ INFO: operator family "isn_ops" of access method hash is missing cross-type ope
btree | issn_ops
btree | upc_ops
hash | ean13_ops
+ hash | gtin14_ops
hash | isbn13_ops
hash | isbn_ops
hash | ismn13_ops
@@ -44,11 +48,20 @@ INFO: operator family "isn_ops" of access method hash is missing cross-type ope
hash | issn13_ops
hash | issn_ops
hash | upc_ops
-(16 rows)
+(18 rows)
--
-- test valid conversions
--
+SELECT '1234567890128'::GTIN14, -- EAN is a GTIN14
+ '123456789012?'::GTIN14, -- compute check digit for me
+ '11234567890125'::GTIN14,
+ '01234567890128'::GTIN14;
+ gtin14 | gtin14 | gtin14 | gtin14
+-----------------+-----------------+-----------------+-----------------
+ 0123456789012-8 | 0123456789012-8 | 1123456789012-5 | 0123456789012-8
+(1 row)
+
SELECT '9780123456786'::EAN13, -- old book
'9790123456785'::EAN13, -- music
'9791234567896'::EAN13, -- new book
@@ -249,6 +262,67 @@ SELECT 9780123456786::ISBN;
ERROR: cannot cast type bigint to isbn
LINE 1: SELECT 9780123456786::ISBN;
^
+SELECT '91234567890125'::GTIN14; -- invalid indicator
+ERROR: Indicator digit out of range for GTIN14 number: "91234567890125"
+LINE 1: SELECT '91234567890125'::GTIN14;
+ ^
+SELECT '123456789012'::GTIN14; -- too short
+ERROR: invalid input syntax for GTIN14 number: "123456789012"
+LINE 1: SELECT '123456789012'::GTIN14;
+ ^
+SELECT '1234567890127'::GTIN14; -- wrong checkdigit
+ERROR: invalid check digit for GTIN14 number: "1234567890127", should be 8
+LINE 1: SELECT '1234567890127'::GTIN14;
+ ^
+--
+-- test validity helpers
+--
+SELECT make_valid('1234567890120!'::GTIN14); -- EAN-13
+ make_valid
+-----------------
+ 0123456789012-8
+(1 row)
+
+SELECT make_valid('11234567890120!'::GTIN14); -- GTIN-14
+ make_valid
+-----------------
+ 1123456789012-5
+(1 row)
+
+SELECT is_valid(make_valid('1234567890120!'::GTIN14)); -- EAN-13
+ is_valid
+----------
+ t
+(1 row)
+
+SELECT is_valid(make_valid('11234567890120!'::GTIN14)); -- GTIN-14
+ is_valid
+----------
+ t
+(1 row)
+
+CREATE TABLE gtin_valid (gtin GTIN14 NOT NULL);
+INSERT INTO gtin_valid VALUES
+-- all invalid because of ! marking
+ ('1234567890120!'), -- invalid EAN-13
+ ('1234567890128!'), -- valid EAN-13
+ ('11234567890120!'), -- invalid GTIN-14
+ ('11234567890125!'), -- valid GTIN-14
+-- valid
+ ('1234567890128'::GTIN14), -- valid EAN-13
+ ('11234567890125'::GTIN14); -- valid GTIN-14
+SELECT gtin, is_valid(gtin) FROM gtin_valid;
+ gtin | is_valid
+------------------+----------
+ 0123456789012-8! | f
+ 0123456789012-8! | f
+ 1123456789012-5! | f
+ 1123456789012-5! | f
+ 0123456789012-8 | t
+ 1123456789012-5 | t
+(6 rows)
+
+DROP TABLE gtin_valid;
--
-- test some comparisons, must yield true
--
diff --git a/isn--1.1.sql b/isn--1.1.sql
index 5206961..fd3f157 100644
--- a/isn--1.1.sql
+++ b/isn--1.1.sql
@@ -15,6 +15,26 @@
-- Input and output functions and data types:
--
---------------------------------------------------
+CREATE FUNCTION gtin14_in(cstring)
+ RETURNS gtin14
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE FUNCTION gtin14_out(gtin14)
+ RETURNS cstring
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE TYPE gtin14 (
+ INPUT = gtin14_in,
+ OUTPUT = gtin14_out,
+ LIKE = pg_catalog.int8
+);
+COMMENT ON TYPE gtin14
+ IS 'Global Trade Item Number (GTIN-14)';
+
CREATE FUNCTION ean13_in(cstring)
RETURNS ean13
AS 'MODULE_PATHNAME'
@@ -181,6 +201,44 @@ COMMENT ON TYPE upc
-- Operator functions:
--
---------------------------------------------------
+-- GTIN-14:
+CREATE FUNCTION isnlt(gtin14, gtin14)
+ RETURNS boolean
+ AS 'int8lt'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE FUNCTION isnle(gtin14, gtin14)
+ RETURNS boolean
+ AS 'int8le'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE FUNCTION isneq(gtin14, gtin14)
+ RETURNS boolean
+ AS 'int8eq'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE FUNCTION isnge(gtin14, gtin14)
+ RETURNS boolean
+ AS 'int8ge'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE FUNCTION isngt(gtin14, gtin14)
+ RETURNS boolean
+ AS 'int8gt'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+CREATE FUNCTION isnne(gtin14, gtin14)
+ RETURNS boolean
+ AS 'int8ne'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+
-- EAN13:
CREATE FUNCTION isnlt(ean13, ean13)
RETURNS boolean
@@ -1236,6 +1294,61 @@ CREATE FUNCTION isnne(upc, ean13)
-- Now the operators:
--
+--
+-- GTIN-14 operators:
+--
+---------------------------------------------------
+CREATE OPERATOR < (
+ PROCEDURE = isnlt,
+ LEFTARG = gtin14,
+ RIGHTARG = gtin14,
+ COMMUTATOR = >,
+ NEGATOR = >=,
+ RESTRICT = scalarltsel,
+ JOIN = scalarltjoinsel);
+CREATE OPERATOR <= (
+ PROCEDURE = isnle,
+ LEFTARG = gtin14,
+ RIGHTARG = gtin14,
+ COMMUTATOR = >=,
+ NEGATOR = >,
+ RESTRICT = scalarltsel,
+ JOIN = scalarltjoinsel);
+CREATE OPERATOR = (
+ PROCEDURE = isneq,
+ LEFTARG = gtin14,
+ RIGHTARG = gtin14,
+ COMMUTATOR = =,
+ NEGATOR = <>,
+ RESTRICT = eqsel,
+ JOIN = eqjoinsel,
+ MERGES,
+ HASHES);
+CREATE OPERATOR >= (
+ PROCEDURE = isnge,
+ LEFTARG = gtin14,
+ RIGHTARG = gtin14,
+ COMMUTATOR = <=,
+ NEGATOR = <,
+ RESTRICT = scalargtsel,
+ JOIN = scalargtjoinsel );
+CREATE OPERATOR > (
+ PROCEDURE = isngt,
+ LEFTARG = gtin14,
+ RIGHTARG = gtin14,
+ COMMUTATOR = <,
+ NEGATOR = <=,
+ RESTRICT = scalargtsel,
+ JOIN = scalargtjoinsel );
+CREATE OPERATOR <> (
+ PROCEDURE = isnne,
+ LEFTARG = gtin14,
+ RIGHTARG = gtin14,
+ COMMUTATOR = <>,
+ NEGATOR = =,
+ RESTRICT = neqsel,
+ JOIN = neqjoinsel);
+
--
-- EAN13 operators:
--
@@ -2708,6 +2821,34 @@ CREATE OPERATOR FAMILY isn_ops USING hash;
-- Operator classes:
--
---------------------------------------------------
+-- GTIN-14:
+CREATE FUNCTION btgtin14cmp(gtin14, gtin14)
+ RETURNS int4
+ AS 'btint8cmp'
+ LANGUAGE 'internal'
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
+
+CREATE OPERATOR CLASS gtin14_ops DEFAULT
+ FOR TYPE gtin14 USING btree FAMILY isn_ops AS
+ OPERATOR 1 <,
+ OPERATOR 2 <=,
+ OPERATOR 3 =,
+ OPERATOR 4 >=,
+ OPERATOR 5 >,
+ FUNCTION 1 btgtin14cmp(gtin14, gtin14);
+
+CREATE FUNCTION hashgtin14(gtin14)
+ RETURNS int4
+ AS 'hashint8'
+ LANGUAGE 'internal' IMMUTABLE STRICT
+ PARALLEL SAFE;
+
+CREATE OPERATOR CLASS gtin14_ops DEFAULT
+ FOR TYPE gtin14 USING hash FAMILY isn_ops AS
+ OPERATOR 1 =,
+ FUNCTION 1 hashgtin14(gtin14);
+
-- EAN13:
CREATE FUNCTION btean13cmp(ean13, ean13)
RETURNS int4
@@ -3314,6 +3455,12 @@ CREATE CAST (issn13 AS issn) WITHOUT FUNCTION AS ASSIGNMENT;
--
-- Validation stuff for lose types:
--
+CREATE FUNCTION make_valid(gtin14)
+ RETURNS gtin14
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
CREATE FUNCTION make_valid(ean13)
RETURNS ean13
AS 'MODULE_PATHNAME'
@@ -3363,6 +3510,12 @@ CREATE FUNCTION make_valid(upc)
IMMUTABLE STRICT
PARALLEL SAFE;
+CREATE FUNCTION is_valid(gtin14)
+ RETURNS boolean
+ AS 'MODULE_PATHNAME'
+ LANGUAGE C
+ IMMUTABLE STRICT
+ PARALLEL SAFE;
CREATE FUNCTION is_valid(ean13)
RETURNS boolean
AS 'MODULE_PATHNAME'
diff --git a/isn.c b/isn.c
index 0c2cac7..d7baa78 100644
--- a/isn.c
+++ b/isn.c
@@ -36,10 +36,10 @@ PG_MODULE_MAGIC;
enum isn_type
{
- INVALID, ANY, EAN13, ISBN, ISMN, ISSN, UPC
+ INVALID, ANY, EAN13, ISBN, ISMN, ISSN, UPC, GTIN14
};
-static const char *const isn_names[] = {"EAN13/UPC/ISxN", "EAN13/UPC/ISxN", "EAN13", "ISBN", "ISMN", "ISSN", "UPC"};
+static const char *const isn_names[] = {"EAN13/UPC/ISxN", "EAN13/UPC/ISxN", "EAN13", "ISBN", "ISMN", "ISSN", "UPC", "GTIN14"};
static bool g_weak = false;
@@ -325,7 +325,10 @@ checkdig(char *num, unsigned size)
}
num++;
}
- check = (check + 3 * check3) % 10;
+ if (pos % 2 == 0) /* for even length strings */
+ check = (check + 3 * check3) % 10;
+ else
+ check = (check3 + 3 * check) % 10;
if (check != 0)
check = 10 - check;
return check;
@@ -500,11 +503,9 @@ ean2UPC(char *isn)
}
/*
- * ean2* --- Converts a string of digits into an ean13 number.
- * Assumes the input string is a string with only digits
- * on it, and that it's within the range of ean13.
+ * str2ean --- Converts a string of digits into binary storage format.
*
- * Returns the ean13 value of the string.
+ * Returns the ean13 with valid-flag set to true.
*/
static ean13
str2ean(const char *num)
@@ -520,6 +521,71 @@ str2ean(const char *num)
return (ean << 1); /* also give room to a flag */
}
+/*
+ * gtin14_2string --- Try to convert GTINumber to a hyphenated string.
+ * Assumes there's enough space in result to hold
+ * the string (maximum MAXEAN13LEN+1 bytes)
+ * This doesn't verify for a valid check digit.
+ *
+ * If errorOK is false, ereport a useful error message if the string is bad.
+ * If errorOK is true, just return "false" for bad input.
+ */
+static bool
+gtin14_2string(ean13 ean, bool errorOK, char *result)
+{
+ enum isn_type type = INVALID;
+
+ char *aux;
+ unsigned digval;
+ unsigned search;
+ char valid = '\0'; /* was the number initially written with a
+ * valid check digit? */
+
+ if ((ean & 1) != 0)
+ valid = '!';
+ ean >>= 1;
+ /* verify it's in the EAN13 range */
+ if (ean > UINT64CONST(99999999999999))
+ goto eantoobig;
+
+ /* convert the number */
+ search = 0;
+ aux = result + MAXEAN13LEN;
+ *aux = '\0'; /* terminate string; aux points to last digit */
+ *--aux = valid; /* append '!' for numbers with invalid but
+ * corrected check digit */
+ do
+ {
+ digval = (unsigned) (ean % 10); /* get the decimal value */
+ ean /= 10; /* get next digit */
+ *--aux = (char) (digval + '0'); /* convert to ascii and store */
+ if (search == 0)
+ *--aux = '-'; /* the check digit is always there */
+ } while (ean && search++ < 14);
+ while (search++ < 14)
+ *--aux = '0'; /* fill the remaining GTIN14 with '0' */
+
+ search = hyphenate(result, result + 2, NULL, NULL);
+
+ return true;
+
+eantoobig:
+ if (!errorOK)
+ {
+ char eanbuf[64];
+
+ /*
+ * Format the number separately to keep the machine-dependent format
+ * code out of the translatable message text
+ */
+ snprintf(eanbuf, sizeof(eanbuf), EAN13_FORMAT, ean);
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value \"%s\" is out of range for %s type",
+ eanbuf, isn_names[type])));
+ }
+ return false;
+}
/*
* ean2string --- Try to convert an ean13 number to a hyphenated string.
* Assumes there's enough space in result to hold
@@ -673,6 +739,162 @@ eantoobig:
return false;
}
+/*
+ * string2gtin14 --- try to parse a string into an gtin14.
+ *
+ * If errorOK is false, ereport a useful error message if the string is bad.
+ * If errorOK is true, just return "false" for bad input.
+ *
+ * if the input string ends with '!' it will always be treated as invalid
+ * (even if the check digit is valid)
+ */
+static bool
+string2gtin14(const char *str, bool errorOK, ean13 *result,
+ enum isn_type accept)
+{
+ bool digit,
+ last;
+ char buf[17] = " ";
+ char *aux1 = buf + 2; /* leave space for the first part, in case
+ * it's needed */
+ const char *aux2 = str;
+ enum isn_type type = INVALID;
+ unsigned check = 0,
+ rcheck = (unsigned) -1;
+ unsigned length = 0;
+ bool magic = false,
+ valid = true;
+
+ /* recognize and validate the number: */
+ while (*aux2 && length <= 14)
+ {
+ last = (*(aux2 + 1) == '!' || *(aux2 + 1) == '\0'); /* is the last character */
+ digit = (isdigit((unsigned char) *aux2) != 0); /* is current character
+ * a digit? */
+ if (*aux2 == '?' && last) /* automagically calculate check digit if
+ * it's '?' */
+ magic = digit = true;
+ if (*aux2 == '!' && *(aux2 + 1) == '\0')
+ {
+ /* the invalid check digit suffix was found, set it */
+ if (!magic)
+ valid = false;
+ magic = true;
+ }
+ else if (!digit)
+ {
+ goto eaninvalid;
+ }
+ else
+ {
+ *aux1++ = *aux2;
+ if (++length > 14)
+ goto eantoobig;
+ }
+ aux2++;
+ }
+ *aux1 = '\0'; /* terminate the string */
+
+ /* find the current check digit value */
+ if (length == 13)
+ {
+ /* is likely EAN13, prefix with 0 to make valid GTIN14 */
+ type = EAN13;
+ buf[1] = '0'; /* prefix 0 */
+ check = buf[14] - '0';
+ }
+ else if (length == 14)
+ {
+ /* is likely GTIN14 */
+ type = GTIN14;
+ check = buf[15] - '0';
+
+ if (buf[2] - '0' == 9) /* valid range of indicator digit is 0-8 */
+ goto indicatorinvalid;
+ }
+ else
+ goto eaninvalid;
+
+ if (type == INVALID)
+ goto eaninvalid;
+
+ /* compute check digit: */
+ for (aux1 = buf; *aux1 && *aux1 <= ' '; aux1++);
+ rcheck = checkdig(aux1, 14);
+ /* validate check digit and convert to gtin14: */
+ switch (type)
+ {
+ case GTIN14:
+ case EAN13:
+ valid = (valid && (rcheck == check || magic));
+ break;
+ default:
+ break;
+ }
+ /* always fix the check digit: */
+ aux1[13] = rcheck + '0';
+ aux1[14] = '\0';
+
+ if (!valid && !magic)
+ goto eanbadcheck;
+
+ *result = str2ean(aux1);
+ *result |= valid ? 0 : 1;
+ return true;
+
+eanbadcheck:
+ if (g_weak)
+ { /* weak input mode is activated: */
+ /* set the "invalid-check-digit-on-input" flag */
+ *result = str2ean(aux1);
+ *result |= 1;
+ return true;
+ }
+
+ if (!errorOK)
+ {
+ if (rcheck == (unsigned) -1)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid %s number: \"%s\"",
+ isn_names[accept], str)));
+ }
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid check digit for %s number: \"%s\", should be %c",
+ isn_names[accept], str, (rcheck == 10) ? ('X') : (rcheck + '0'))));
+ }
+ }
+ return false;
+
+eaninvalid:
+ if (!errorOK)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for %s number: \"%s\"",
+ isn_names[accept], str)));
+ return false;
+
+indicatorinvalid:
+ if (!errorOK)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("Indicator digit out of range for %s number: \"%s\"",
+ isn_names[accept], str)));
+ return false;
+
+eantoobig:
+ if (!errorOK)
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value \"%s\" is out of range for %s type",
+ str, isn_names[accept])));
+ return false;
+}
+
/*
* string2ean --- try to parse a string into an ean13.
*
@@ -990,6 +1212,35 @@ ean13_in(PG_FUNCTION_ARGS)
PG_RETURN_EAN13(result);
}
+/* gtin14_out
+ */
+PG_FUNCTION_INFO_V1(gtin14_out);
+Datum
+gtin14_out(PG_FUNCTION_ARGS)
+{
+ ean13 val = PG_GETARG_EAN13(0);
+ char *result;
+ char buf[MAXEAN13LEN + 1];
+
+ (void) gtin14_2string(val, false, buf);
+
+ result = pstrdup(buf);
+ PG_RETURN_CSTRING(result);
+}
+
+/* gtin14_in
+ */
+PG_FUNCTION_INFO_V1(gtin14_in);
+Datum
+gtin14_in(PG_FUNCTION_ARGS)
+{
+ const char *str = PG_GETARG_CSTRING(0);
+ ean13 result;
+
+ (void) string2gtin14(str, false, &result, GTIN14);
+ PG_RETURN_EAN13(result);
+}
+
/* isbn_in
*/
PG_FUNCTION_INFO_V1(isbn_in);
diff --git a/sql/isn.sql b/sql/isn.sql
index 71577d5..ede461f 100644
--- a/sql/isn.sql
+++ b/sql/isn.sql
@@ -16,6 +16,11 @@ WHERE NOT amvalidate(oid);
--
-- test valid conversions
--
+SELECT '1234567890128'::GTIN14, -- EAN is a GTIN14
+ '123456789012?'::GTIN14, -- compute check digit for me
+ '11234567890125'::GTIN14,
+ '01234567890128'::GTIN14;
+
SELECT '9780123456786'::EAN13, -- old book
'9790123456785'::EAN13, -- music
'9791234567896'::EAN13, -- new book
@@ -100,6 +105,30 @@ SELECT 'postgresql...'::ISBN;
SELECT 9780123456786::EAN13;
SELECT 9780123456786::ISBN;
+SELECT '91234567890125'::GTIN14; -- invalid indicator
+SELECT '123456789012'::GTIN14; -- too short
+SELECT '1234567890127'::GTIN14; -- wrong checkdigit
+
+--
+-- test validity helpers
+--
+SELECT make_valid('1234567890120!'::GTIN14); -- EAN-13
+SELECT make_valid('11234567890120!'::GTIN14); -- GTIN-14
+SELECT is_valid(make_valid('1234567890120!'::GTIN14)); -- EAN-13
+SELECT is_valid(make_valid('11234567890120!'::GTIN14)); -- GTIN-14
+
+CREATE TABLE gtin_valid (gtin GTIN14 NOT NULL);
+INSERT INTO gtin_valid VALUES
+-- all invalid because of ! marking
+ ('1234567890120!'), -- invalid EAN-13
+ ('1234567890128!'), -- valid EAN-13
+ ('11234567890120!'), -- invalid GTIN-14
+ ('11234567890125!'), -- valid GTIN-14
+-- valid
+ ('1234567890128'::GTIN14), -- valid EAN-13
+ ('11234567890125'::GTIN14); -- valid GTIN-14
+SELECT gtin, is_valid(gtin) FROM gtin_valid;
+DROP TABLE gtin_valid;
--
-- test some comparisons, must yield true
--