feat(ctype): classification and case-mapping tables

This commit is contained in:
2026-09-03 20:03:07 -04:00
parent 858fc1762c
commit 20f514d91c
5 changed files with 691 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <ctype.h>
/*
* The "C" locale character-class table: one entry per unsigned char value,
* holding the OR of the class bits below. The table is laid out as an
* explicit 8-entries-per-row grid so each byte can be audited against the
* ASCII ranges the standard specifies. The 0x80..0xff half is deliberately
* not written: a static initializer zero-fills the elements it does not
* mention, so every high byte — entry 255 included, which is where EOF
* lands after the unsigned char cast — classifies false. Because every
* lookup casts the argument to unsigned char first:
*
* - a negative char value can never index out of bounds;
* - EOF classifies false in every table-driven is* function without a
* special case;
* - tolower/toupper pass EOF (and every non-letter) through unchanged,
* returning the argument as given.
*
* isdigit, isxdigit and isprint are not table-driven: they are single
* unsigned comparisons on the argument (see below), which makes them
* genuinely const-capable — but only isdigit/isxdigit are declared const:
* GCC predeclares isdigit/isxdigit as const builtins and isprint as a pure
* builtin, and the header's attribute must match each builtin's declaration
* or the weaker one is rejected with a warning.
*/
#define CTYPE_UPPER 0x01U
#define CTYPE_LOWER 0x02U
#define CTYPE_DIGIT 0x04U
#define CTYPE_CNTRL 0x08U
#define CTYPE_PUNCT 0x10U
#define CTYPE_SPACE 0x20U
#define CTYPE_BLANK 0x80U
/* Combined masks, mirroring the standard's definitions. */
#define CTYPE_ALPHA (CTYPE_UPPER | CTYPE_LOWER)
#define CTYPE_ALNUM (CTYPE_ALPHA | CTYPE_DIGIT)
#define CTYPE_GRAPH (CTYPE_ALNUM | CTYPE_PUNCT)
static const unsigned char ctype_class[256] = {
/* 0 1 2 3 4 5 6 7 */
/* 0x00 */ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
/* 0x08 */ 0x08, 0xa8, 0x28, 0x28, 0x28, 0x28, 0x08, 0x08,
/* 0x10 */ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
/* 0x18 */ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
/* 0x20 */ 0xa0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x28 */ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x30 */ 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
/* 0x38 */ 0x04, 0x04, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x40 */ 0x10, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x48 */ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x50 */ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
/* 0x58 */ 0x01, 0x01, 0x01, 0x10, 0x10, 0x10, 0x10, 0x10,
/* 0x60 */ 0x10, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
/* 0x68 */ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
/* 0x70 */ 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
/* 0x78 */ 0x02, 0x02, 0x02, 0x10, 0x10, 0x10, 0x10, 0x08,
/* 0x80..0xff stay zero by the static initializer's default. */
};
int
isalnum(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_ALNUM) != 0;
}
int
isalpha(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_ALPHA) != 0;
}
int
isblank(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_BLANK) != 0;
}
int
iscntrl(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_CNTRL) != 0;
}
/*
* Pure arithmetic (no table): the unsigned cast turns every value below '0'
* into a huge number, so exactly 0..9 pass the range check.
*/
int
isdigit(int c)
{
return (unsigned int)c - '0' < 10U;
}
int
isgraph(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_GRAPH) != 0;
}
int
islower(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_LOWER) != 0;
}
/*
* Pure arithmetic (no table): the printable range 0x20..0x7e.
*/
int
isprint(int c)
{
return (unsigned int)c - 0x20 < 0x5fU;
}
int
ispunct(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_PUNCT) != 0;
}
int
isspace(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_SPACE) != 0;
}
int
isupper(int c)
{
return (ctype_class[(unsigned char)c] & CTYPE_UPPER) != 0;
}
/*
* Pure arithmetic (no table): c | 0x20 folds 'A'..'F' to 'a'..'f'; the
* unsigned cast first keeps every negative value out of the range check.
*/
int
isxdigit(int c)
{
return (unsigned int)c - '0' < 10U || (unsigned int)(c | 0x20) - 'a' < 6U;
}
/*
* Map an uppercase letter to lowercase and pass everything else — non-letters
* and EOF alike — through unchanged. The class bit is authoritative, so the
* unsigned char cast already keeps EOF and negative values out of the
* mapping branch.
*/
int
tolower(int c)
{
if ((ctype_class[(unsigned char)c] & CTYPE_UPPER) != 0)
{
return c + ('a' - 'A');
}
return c;
}
int
toupper(int c)
{
if ((ctype_class[(unsigned char)c] & CTYPE_LOWER) != 0)
{
return c - ('a' - 'A');
}
return c;
}
+15
View File
@@ -0,0 +1,15 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <ctype.h>
/*
* True when c is a 7-bit ASCII value. EOF and every other negative value are
* outside the range, so they classify false without a cast.
*/
int
isascii(int c)
{
return c >= 0 && c < 128;
}
+15
View File
@@ -0,0 +1,15 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <ctype.h>
/*
* Clear every bit above the low 7 of c. Pure bit arithmetic: defined for
* every int value, EOF included (toascii(-1) is 0x7f).
*/
int
toascii(int c)
{
return c & 0x7f;
}