1/*-------------------------------------------------------------------------
4 * PostgreSQL type definitions for ISNs (ISBN, ISMN, ISSN, EAN13, UPC)
6 * Author: German Mendez Bravo (Kronuz)
7 * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
12 *-------------------------------------------------------------------------
31#ifdef USE_ASSERT_CHECKING
37 #define MAXEAN13LEN 18
44 static const char *
const isn_names[] = {
"EAN13/UPC/ISxN",
"EAN13/UPC/ISxN",
"EAN13",
"ISBN",
"ISMN",
"ISSN",
"UPC"};
50/***********************************************************************
52 ** Routines for EAN13/UPC/ISxNs.
55 ** In this code, a normalized string is one that is known to be a valid
56 ** ISxN number containing only digits and hyphens and with enough space
57 ** to hold the full 13 digits plus the maximum of four hyphens.
58 ***********************************************************************/
60/*----------------------------------------------------------
62 *---------------------------------------------------------*/
65 * Check if the table and its index is correct (just for debugging)
69 check_table(const
char *(*TABLE)[2], const
unsigned TABLE_index[10][2])
84 while (TABLE[
i][0] && TABLE[
i][1])
89 /* must always start with a digit: */
90 if (!isdigit((
unsigned char) *aux1) || !isdigit((
unsigned char) *aux2))
95 /* must always have the same format and length: */
96 while (*aux1 && *aux2)
98 if (!(isdigit((
unsigned char) *aux1) &&
99 isdigit((
unsigned char) *aux2)) &&
100 (*aux1 != *aux2 || *aux1 !=
'-'))
108 /* found a new range */
111 /* check current range in the index: */
112 for (
j =
x;
j <=
y;
j++)
123 /* Always get the new limit */
133 elog(
DEBUG1,
"invalid table near {\"%s\", \"%s\"} (pos: %d)",
134 TABLE[
i][0], TABLE[
i][1],
i);
142/*----------------------------------------------------------
143 * Formatting and conversion routines.
144 *---------------------------------------------------------*/
153 if (isdigit((
unsigned char) *bufI))
165 * hyphenate --- Try to hyphenate, in-place, the string starting at bufI
166 * into bufO using the given hyphenation range TABLE.
167 * Assumes the input string to be used is of only digits.
169 * Returns the number of characters actually hyphenated.
175 const char *ean_aux1,
188 /* just compress the string if no further hyphenation is required */
200 /* add remaining hyphenations */
202 search = *bufI -
'0';
210 search =
lower + step;
213 ean_in1 = ean_in2 =
false;
214 ean_aux1 = TABLE[search][0];
215 ean_aux2 = TABLE[search][1];
218 if ((ean_in1 || *firstdig >= *ean_aux1) && (ean_in2 || *firstdig <= *ean_aux2))
220 if (*firstdig > *ean_aux1)
222 if (*firstdig < *ean_aux2)
224 if (ean_in1 && ean_in2)
227 firstdig++, ean_aux1++, ean_aux2++;
228 if (!(*ean_aux1 && *ean_aux2 && *firstdig))
230 if (!isdigit((
unsigned char) *ean_aux1))
231 ean_aux1++, ean_aux2++;
236 * check in what direction we should go and move the pointer
239 if (*firstdig < *ean_aux1 && !ean_in1)
245 search =
lower + step;
247 /* Initialize stuff again: */
249 ean_in1 = ean_in2 =
false;
250 ean_aux1 = TABLE[search][0];
251 ean_aux2 = TABLE[search][1];
259 ean_p = TABLE[search][0];
260 while (*ean_p && *aux2)
269 *aux1 = *aux2;
/* add a lookahead char */
276 * weight_checkdig -- Receives a buffer with a normalized ISxN string number,
277 * and the length to weight.
279 * Returns the weight of the number (the check digit value, 0-10)
286 while (*isn && size > 1)
288 if (isdigit((
unsigned char) *isn))
290 weight += size-- * (*isn -
'0');
294 weight = weight % 11;
296 weight = 11 - weight;
302 * checkdig --- Receives a buffer with a normalized ISxN string number,
303 * and the length to check.
305 * Returns the check digit value (0-9)
315 {
/* ISMN start with 'M' */
319 while (*num && size > 1)
321 if (isdigit((
unsigned char) *num))
324 check3 += *num -
'0';
331 check = (check + 3 * check3) % 10;
338 * ean2isn --- Try to convert an ean13 number to a UPC/ISxN number.
339 * This doesn't verify for a valid check digit.
341 * If errorOK is false, ereport a useful error message if the ean13 is bad.
342 * If errorOK is true, just return "false" for bad input.
356 /* verify it's in the EAN13 range */
360 /* convert the number */
363 *aux =
'0円';
/* terminate string; aux points to last digit */
366 digval = (unsigned) (ean % 10);
/* get the decimal value */
367 ean /= 10;
/* get next digit */
368 *--aux = (char) (digval +
'0');
/* convert to ascii and store */
369 }
while (ean && search++ < 12);
370 while (search++ < 12)
371 *--aux =
'0';
/* fill the remaining EAN13 with '0' */
373 /* find out the data type: */
374 if (strncmp(
"978",
buf, 3) == 0)
378 else if (strncmp(
"977",
buf, 3) == 0)
382 else if (strncmp(
"9790",
buf, 4) == 0)
386 else if (strncmp(
"979",
buf, 3) == 0)
390 else if (*
buf ==
'0')
410 (
errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
411 errmsg(
"cannot cast EAN13(%s) to %s for number: \"%s\"",
417 (
errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
418 errmsg(
"cannot cast %s to %s for number: \"%s\"",
430 * Format the number separately to keep the machine-dependent format
431 * code out of the translatable message text
435 (
errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
436 errmsg(
"value \"%s\" is out of range for %s type",
443 * ean2UPC/ISxN --- Convert in-place a normalized EAN13 string to the corresponding
444 * UPC/ISxN string number. Assumes the input string is normalized.
453 * The number should come in this format: 978-0-000-00000-0 or may be an
454 * ISBN-13 number, 979-..., which does not have a short representation. Do
455 * the short output version if possible.
457 if (strncmp(
"978-", isn, 4) == 0)
459 /* Strip the first part and calculate the new check digit */
462 aux = strchr(isn,
'0円');
463 while (!isdigit((
unsigned char) *--aux));
474 /* the number should come in this format: 979-0-000-00000-0 */
475 /* Just strip the first part and change the first digit ('0') to 'M' */
485 /* the number should come in this format: 977-0000-000-00-0 */
486 /* Strip the first part, crop, and calculate the new check digit */
492 isn[8] = check +
'0';
499 /* the number should come in this format: 000-000000000-0 */
500 /* Strip the first part, crop, and dehyphenate */
506 * ean2* --- Converts a string of digits into an ean13 number.
507 * Assumes the input string is a string with only digits
508 * on it, and that it's within the range of ean13.
510 * Returns the ean13 value of the string.
515 ean13 ean = 0;
/* current ean */
519 if (isdigit((
unsigned char) *num))
520 ean = 10 * ean + (*num -
'0');
523 return (ean << 1);
/* also give room to a flag */
527 * ean2string --- Try to convert an ean13 number to a hyphenated string.
528 * Assumes there's enough space in result to hold
529 * the string (maximum MAXEAN13LEN+1 bytes)
530 * This doesn't verify for a valid check digit.
532 * If shortType is true, the returned string is in the old ISxN short format.
533 * If errorOK is false, ereport a useful error message if the string is bad.
534 * If errorOK is true, just return "false" for bad input.
539 const char *(*TABLE)[2];
546 char valid =
'0円';
/* was the number initially written with a
547 * valid check digit? */
554 /* verify it's in the EAN13 range */
558 /* convert the number */
561 *aux =
'0円';
/* terminate string; aux points to last digit */
562 *--aux = valid;
/* append '!' for numbers with invalid but
563 * corrected check digit */
566 digval = (unsigned) (ean % 10);
/* get the decimal value */
567 ean /= 10;
/* get next digit */
568 *--aux = (char) (digval +
'0');
/* convert to ascii and store */
570 *--aux =
'-';
/* the check digit is always there */
571 }
while (ean && search++ < 13);
572 while (search++ < 13)
573 *--aux =
'0';
/* fill the remaining EAN13 with '0' */
575 /* The string should be in this form: ???DDDDDDDDDDDD-D" */
578 /* verify it's a logically valid EAN13 */
581 search =
hyphenate(result, result + 3, NULL, NULL);
585 /* find out what type of hyphenation is needed: */
586 if (strncmp(
"978-", result, search) == 0)
587 {
/* ISBN -13 978-range */
588 /* The string should be in this form: 978-??000000000-0" */
593 else if (strncmp(
"977-", result, search) == 0)
595 /* The string should be in this form: 977-??000000000-0" */
600 else if (strncmp(
"979-0", result, search + 1) == 0)
602 /* The string should be in this form: 979-0?000000000-0" */
607 else if (strncmp(
"979-", result, search) == 0)
608 {
/* ISBN-13 979-range */
609 /* The string should be in this form: 979-??000000000-0" */
614 else if (*result ==
'0')
616 /* The string should be in this form: 000-00000000000-0" */
628 /* verify it's a logically valid EAN13/UPC/ISxN */
632 /* verify it's a valid EAN13 */
635 search =
hyphenate(result + digval, result + digval + 2, NULL, NULL);
640 /* convert to the old short type: */
667 * Format the number separately to keep the machine-dependent format
668 * code out of the translatable message text
672 (
errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
673 errmsg(
"value \"%s\" is out of range for %s type",
680 * string2ean --- try to parse a string into an ean13.
682 * ereturn false with a useful error message if the string is bad.
683 * Otherwise return true.
685 * if the input string ends with '!' it will always be treated as invalid
686 * (even if the check digit is valid)
695 char *aux1 =
buf + 3;
/* leave space for the first part, in case
697 const char *aux2 =
str;
700 rcheck = (unsigned) -1;
705 /* recognize and validate the number: */
706 while (*aux2 && length <= 13)
708 last = (*(aux2 + 1) ==
'!' || *(aux2 + 1) ==
'0円');
/* is the last character */
709 digit = (isdigit((
unsigned char) *aux2) != 0);
/* is current character
711 if (*aux2 ==
'?' && last)
/* automagically calculate check digit if
713 magic = digit =
true;
714 if (length == 0 && (*aux2 ==
'M' || *aux2 ==
'm'))
716 /* only ISMN can be here */
723 else if (length == 7 && (digit || *aux2 ==
'X' || *aux2 ==
'x') && last)
725 /* only ISSN can be here */
732 else if (length == 9 && (digit || *aux2 ==
'X' || *aux2 ==
'x') && last)
734 /* only ISBN and ISMN can be here */
738 type =
ISBN;
/* ISMN must start with 'M' */
742 else if (length == 11 && digit && last)
744 /* only UPC can be here */
751 else if (*aux2 ==
'-' || *aux2 ==
' ')
753 /* skip, we could validate but I think it's worthless */
755 else if (*aux2 ==
'!' && *(aux2 + 1) ==
'0円')
757 /* the invalid check digit suffix was found, set it */
774 *aux1 =
'0円';
/* terminate the string */
776 /* find the current check digit value */
779 /* only EAN13 can be here */
783 check =
buf[15] -
'0';
785 else if (length == 12)
787 /* only UPC can be here */
790 check =
buf[14] -
'0';
792 else if (length == 10)
799 check =
buf[12] -
'0';
801 else if (length == 8)
809 check =
buf[10] -
'0';
817 /* obtain the real check digit value, validate, and convert to ean13: */
825 valid = (valid && ((rcheck =
checkdig(
buf + 3, 13)) == check || magic));
826 /* now get the subtype of EAN13: */
829 else if (strncmp(
"977",
buf + 3, 3) == 0)
831 else if (strncmp(
"978",
buf + 3, 3) == 0)
833 else if (strncmp(
"9790",
buf + 3, 4) == 0)
835 else if (strncmp(
"979",
buf + 3, 3) == 0)
841 memcpy(
buf,
"9790", 4);
/* this isn't for sure yet, for now ISMN
843 valid = (valid && ((rcheck =
checkdig(
buf, 13)) == check || magic));
846 memcpy(
buf,
"978", 3);
850 memcpy(
buf + 10,
"00", 2);
/* append 00 as the normal issue
851 * publication code */
852 memcpy(
buf,
"977", 3);
857 valid = (valid && ((rcheck =
checkdig(
buf + 2, 13)) == check || magic));
862 /* fix the check digit: */
863 for (aux1 =
buf; *aux1 && *aux1 <=
' '; aux1++);
864 aux1[12] =
checkdig(aux1, 13) +
'0';
867 if (!valid && !magic)
871 *result |= valid ? 0 : 1;
876 {
/* weak input mode is activated: */
877 /* set the "invalid-check-digit-on-input" flag */
883 if (rcheck == (
unsigned) -1)
886 (
errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
887 errmsg(
"invalid %s number: \"%s\"",
893 (
errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
894 errmsg(
"invalid check digit for %s number: \"%s\", should be %c",
900 (
errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
901 errmsg(
"invalid input syntax for %s number: \"%s\"",
906 (
errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
907 errmsg(
"cannot cast %s to %s for number: \"%s\"",
912 (
errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
913 errmsg(
"value \"%s\" is out of range for %s type",
917/*----------------------------------------------------------
919 *---------------------------------------------------------*/
938 /* Define a GUC variable for weak mode. */
940 "Accept input with invalid ISN check digits.",
1106/* is_valid - returns false if the "invalid-check-digit-on-input" is set
1117/* make_valid - unsets the "invalid-check-digit-on-input" flag
1129/* this function temporarily sets weak input flag
1130 * (to lose the strictness of check digit acceptance)
static const unsigned EAN13_index[10][2]
static const char * EAN13_range[][2]
static const char * ISBN_range[][2]
static const unsigned ISBN_index[10][2]
static const unsigned ISBN_index_new[10][2]
static const char * ISBN_range_new[][2]
static const char * ISMN_range[][2]
static const unsigned ISMN_index[10][2]
static const unsigned ISSN_index[10][2]
static const char * ISSN_range[][2]
static const unsigned UPC_index[10][2]
static const char * UPC_range[][2]
int errcode(int sqlerrcode)
int errmsg(const char *fmt,...)
#define ereturn(context, dummy_value,...)
#define ereport(elevel,...)
#define PG_RETURN_CSTRING(x)
#define PG_GETARG_CSTRING(n)
#define PG_GETARG_BOOL(n)
#define PG_RETURN_BOOL(x)
void DefineCustomBoolVariable(const char *name, const char *short_desc, const char *long_desc, bool *valueAddr, bool bootValue, GucContext context, int flags, GucBoolCheckHook check_hook, GucBoolAssignHook assign_hook, GucShowHook show_hook)
void MarkGUCPrefixReserved(const char *className)
int set_config_option(const char *name, const char *value, GucContext context, GucSource source, GucAction action, bool changeVal, int elevel, bool is_reload)
Datum upc_cast_from_ean13(PG_FUNCTION_ARGS)
Datum ean13_out(PG_FUNCTION_ARGS)
Datum issn_in(PG_FUNCTION_ARGS)
PG_MODULE_MAGIC_EXT(.name="isn",.version=PG_VERSION)
Datum isn_out(PG_FUNCTION_ARGS)
static bool string2ean(const char *str, struct Node *escontext, ean13 *result, enum isn_type accept)
Datum make_valid(PG_FUNCTION_ARGS)
static void ean2ISBN(char *isn)
Datum ismn_in(PG_FUNCTION_ARGS)
Datum accept_weak_input(PG_FUNCTION_ARGS)
static bool ean2string(ean13 ean, bool errorOK, char *result, bool shortType)
const unsigned TABLE_index[10][2]
Datum weak_input_status(PG_FUNCTION_ARGS)
Datum upc_in(PG_FUNCTION_ARGS)
static bool ean2isn(ean13 ean, bool errorOK, ean13 *result, enum isn_type accept)
static const char *const isn_names[]
Datum isbn_in(PG_FUNCTION_ARGS)
Datum issn_cast_from_ean13(PG_FUNCTION_ARGS)
static void ean2UPC(char *isn)
Datum ean13_in(PG_FUNCTION_ARGS)
Datum ismn_cast_from_ean13(PG_FUNCTION_ARGS)
Datum isbn_cast_from_ean13(PG_FUNCTION_ARGS)
static unsigned hyphenate(char *bufO, char *bufI, const char *(*TABLE)[2], const unsigned TABLE_index[10][2])
static unsigned checkdig(char *num, unsigned size)
static void ean2ISMN(char *isn)
Datum is_valid(PG_FUNCTION_ARGS)
PG_FUNCTION_INFO_V1(isn_out)
static unsigned dehyphenate(char *bufO, char *bufI)
static unsigned weight_checkdig(char *isn, unsigned size)
static ean13 str2ean(const char *num)
pg_attribute_unused() static bool check_table(const char *(*TABLE)[2]
static void ean2ISSN(char *isn)
#define PG_GETARG_EAN13(n)
#define PG_RETURN_EAN13(x)
char * pstrdup(const char *in)
Datum lower(PG_FUNCTION_ARGS)
Datum upper(PG_FUNCTION_ARGS)
unsigned char pg_ascii_toupper(unsigned char ch)
#define accept(s, addr, addrlen)