Pull Requests and Contributions are Welcomed
Small C utility library macros and functions for char handling. e.g. conversion or checks for hex, digits and ascii values
You are encourage to copy and paste snippets as you need in your project, but you can install it as a normal C header file as well!
Note that some of these macro already have a C standard library equvalent, so you are encourage to use C standard library function for portability.
If you are writing hosted C on a modern desktop or server, you probably don't need this —
isdigit() and friends will be inlined by the compiler at -O2 and the difference disappears.
This library is aimed at two cases where <ctype.h> falls short:
1. Embedded / bare-metal C without a standard library
Many microcontrollers either lack <ctype.h> entirely or ship it behind a paid compiler
licence. This header has no dependencies and drops into any C project with a single file copy.
2. Constrained compilers with optimisation off
On most libc implementations (glibc, newlib), isdigit() is not a simple comparison —
it performs a locale table lookup through thread-local storage on every call:
/* typical glibc implementation */
return __ctype_b_loc()[c] & _ISdigit; /* pointer dereference + TLS indirection */Many embedded compilers run without optimisation (locked behind a paywall, or disabled to avoid compiler bugs), so this overhead is real. The macros here compile to a single unsigned compare regardless of optimisation level.
The FAST_ variants go further — they skip all validation and are pure arithmetic, useful
in tight inner loops (e.g. parsing a high-baud serial stream) where input is already known good.
The library consists of one header file, easily integrated into your project manually or via clib
clib install mofosyne/char-utils.hor copy these files manually to your source folder:
char-utils.h#include "char-utils.h"
/* Character type checks */
if (IS_DIGIT(c)) { /* '0'-'9' */ }
if (IS_HEX_DIGIT(c)) { /* '0'-'9', 'a'-'f', 'A'-'F' */ }
if (IS_ALPHA(c)) { /* 'a'-'z', 'A'-'Z' */ }
if (IS_SPACE(c)) { /* ' ', '\t', '\n', '\r', '\f', '\v' */ }
/* Conversions */
int digit = ASCII_TO_DIGIT(c, -1); /* '7' -> 7, invalid -> -1 */
int hex = HEX_TO_INT(c, -1); /* 'F' -> 15, invalid -> -1 */
char upper = TO_UPPER(c); /* 'a' -> 'A', non-alpha unchanged */
char lo_hex = NIBBLE_TO_LOWERCASE_HEX(n, '?'); /* 10 -> 'a' */
/* Nibble splitting */
uint8_t hi = HIGH_NIBBLE(byte); /* 0xAB -> 0xA */
uint8_t lo = LOW_NIBBLE(byte); /* 0xAB -> 0xB */
/* ASCII diagnostics (safe with getchar() return value) */
int c = getchar();
printf("%s\n", ascii_to_diagnostics(c, "[EOF]")); /* prints e.g. "[LF]" or "A" */Each safe macro accepts an explicit DEFAULT value returned on invalid input.
FAST_ variants skip validation and are suitable when input is already known-good.
| Macro | Equivalent | Description |
|---|---|---|
IS_BINARY(ch) |
— | '0' or '1' |
IS_OCTAL(ch) |
— | '0'–'7' |
IS_DIGIT(ch) |
isdigit() |
'0'–'9' |
IS_LOWER(ch) |
islower() |
'a'–'z' |
IS_UPPER(ch) |
isupper() |
'A'–'Z' |
IS_ALPHA(ch) |
isalpha() |
'a'–'z', 'A'–'Z' |
IS_ALNUM(ch) |
isalnum() |
alphanumeric |
IS_HEX_DIGIT(ch) |
isxdigit() |
'0'–'9', 'a'–'f', 'A'–'F' |
IS_PRINTABLE(ch) |
isprint() |
' '–'~' |
IS_SPACE(ch) |
isspace() |
whitespace (C locale) |
IS_PUNCT(ch) |
ispunct() |
printable, non-alnum, non-space |
IS_BRACKET(ch) |
— | (, ), [, ], {, } |
IS_SYMBOL(ch) |
— | printable punct, non-bracket |
IS_ASCII(ch) |
— | 0–127 |
IS_EXTENDED_ASCII(ch) |
— | 0–255 |
Each has a _GROKKABLE variant with explicit range comparisons for readability.
| Macro | Description |
|---|---|
TO_UPPER(ch) |
Lower to upper, non-alpha unchanged |
TO_LOWER(ch) |
Upper to lower, non-alpha unchanged |
TOGGLE_CASE(ch) |
Flip case, non-alpha unchanged |
FAST_TO_UPPER(ch) |
Bit-clear, no guard |
FAST_TO_LOWER(ch) |
Bit-set, no guard |
FAST_TOGGLE_CASE(ch) |
Bit-flip, no guard |
| Macro | Description |
|---|---|
ASCII_TO_DIGIT(ch, DEFAULT) |
'0'–'9' → 0–9 |
DIGIT_TO_ASCII(n, DEFAULT) |
0–9 → '0'–'9' |
ASCII_TO_BINARY(ch, DEFAULT) |
'0'/'1' → 0/1 |
ASCII_TO_OCTAL(ch, DEFAULT) |
'0'–'7' → 0–7 |
HEX_TO_INT(ch, DEFAULT) |
hex char → 0–15 |
NIBBLE_TO_UPPERCASE_HEX(n, DEFAULT) |
0–15 → '0'–'F' |
NIBBLE_TO_LOWERCASE_HEX(n, DEFAULT) |
0–15 → '0'–'f' |
FAST_ variants available for all of the above.
| Macro | Description |
|---|---|
HIGH_NIBBLE(byte) |
Upper 4 bits |
LOW_NIBBLE(byte) |
Lower 4 bits |
const char *ascii_to_diagnostics(int ch, const char *default_str);Returns a human-readable string for any byte value (e.g. "[LF]", "[NUL]", "A").
Accepts the int return type of getchar()/fgetc() directly; returns default_str for values outside 0–255.