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 --