String Formatting module

Cnx's formatting API creates a composable, ergonomic, human-readable way to format data into strings for storage or I/O.

The interface and functionality is similar to C++'s std::format, and the popular fmtlib it's derived from, and Rust's std::format. Example:

let x = 0.0F;
let y = 1.0F;
// formats x and y as normal decimal format with one significant figure after the decimal point
let formatted = cnx_format("x: {d1}, y: {d1}", x, y);

Formatting supports custom specifiers within the brackets, "{}". Any character except curly brackets ('{' and '}') are valid to use for format specifies for custom types.

Builtin types provide some limited formatting options. For integral types, such as char, u32, or pointers, these include options for notation: 'd' (Decimal, ie Base 10), 'x' (Lower-case Hex), or 'X' (Upper-case Hex). For floating point numbers, such as f32, this include options for notation: 'd' (Decimal) or 'e' (Scientific/Exponential notation), and for the number of significant figures after the decimal point: a number directly following the notation specifier, or on its own if no specifier was given. The default for integral types other than pointer is decimal, for pointers is lower-case hexadecimal, and for pointers is scientific notation with 3 significant figures after the decimal point. In addition to these specifiers, builtin types also accept a "Debug" specifier D, and D should be used as the standard specifier to indicate a debugging format. All Cnx library types providing a CnxFormat implementation (such as CnxDuration or CnxString) support the D specifier and provide a debugging format.

Bools are a special case among builtin types, in that they only support the D specifier, even though they are technically integral types. Bools with always format to directly to "true" or "false".

String formatting is extensible and composable in Cnx because it uses a Trait, CnxFormat, to enable user-defined types to provide their own formatting implementations. This trait provides the functionality for validating format specifiers and performing string formatting for an implementing type.

To provide an implementation of CnxFormat for your type, only three functions and the Trait implementation are required. The functions take the following signatures:

CnxFormatContext (*const is_specifier_valid)(const CnxFormat* restrict self,
                                                  CnxStringView specifier);
CnxString (*const format)(const CnxFormat* restrict self, CnxFormatContext context);
CnxString (*const format_with_allocator)(const CnxFormat* restrict self,
                                              CnxFormatContext context,
                                              CnxAllocator allocator);

is_specifier_valid takes the specifier in the format string, should validate it, and return the result in a CnxFormatContext. A CnxFormatContext stores the validation result as well as up to 32 bytes of aligned storage to store any interim state parsed from the format specifier necessary for formatting your type. For example, implementations for Cnx types store whether the debug specifier D occurred and other state, such as the number of significant figures to use, in the case of floating point types. This context will later be passed to your formatting function in order to format the instance of your type.

format and format_with_allocator use your type (pointed to in the trait object, self) and the CnxFormatContext (that was obtained when Cnx called your is_specifier_valid function) to perform the string formatting. This will likely be implemented in terms of another call to cnx_format, formatting the constituent parts of your type. In addition, format is generally implemented in terms of calling format_with_allocator with the default Cnx allocator.

Arguments passed to cnx_format must be l-values cast to their CnxFormat trait object representation with as_format_t(T, value) or as_trait(CnxFormat, T, value). Cnx will automatically cast builtin types, cstrings, string-literals, and CnxString and CnxStringView to their CnxFormat implementation. Generally, other types will need to be cast explicitly, however, automatic conversions for specific user defined types can be enabled by defining a pair of macros prior to including "CnxFormat.h". First, define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES to TRUE. Then, define CNX_AS_FORMAT_USER_SUPPLIED_TYPES to a comma separated list of types and their conversions. For example:

#define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES TRUE
#define CNX_AS_FORMAT_USER_SUPPLIED_TYPES T*        : as_format_t(T, x),    \
                                          const T*  : as_format_t(T2, x),   \
                                          T2*       : as_format_t(T2, x),   \
                                          const T2* : as_format_t(T2, x),

where T and T2 are your supplied types. x should always be the second argument to the conversion function (literally always give x as the second argument). cnx_format(format_string, ...) automatically applies the as_format(x) _Generic macro to all format arguments in the parameter pack to perform the automatic conversion to CnxFormat. That is why this syntax is necessary for the CNX_AS_FORMAT_USER_SUPPLIED types macro, to provide valid match arms in the as_format(x) macro for that conversion.

A complete example of implementing and using CnxFormat:

typedef struct Point2D {
    f32 x;
    f32 y;
} Point2D;

#define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES TRUE
#define CNX_AS_FORMAT_USER_SUPPLIED_TYPES Point2D*          : as_format_t(Point2D, x),  \
                                          const Point2D*    : as_format_t(Point2D, x),

#include <Cnx/Format.h>

// `specifier` will be a string view over the characters making up the format specifier.
// For example, in the specifier `{e}`, `specifier` would view `e`.
// For this example, we'll make `d` (for decimal/normal notation) or `e`
// (for scientific/exponential notation) followed by a number (ie `4` or `10`, for the number of
// significant figures) a valid specifier, and anything else invalid.
// This is the same set of options available to floating point numbers, so we can just forward
// our implementation to that one.
inline static CnxFormatContext point2d_is_format_specifier_valid(const CnxFormat* restrict self,
                                                                 CnxStringView specifier)
{
    let an_f32 = static_cast(f32)(0.0);
    let format_obj = as_format_t(f32, an_f32);
    return trait_call(is_specifier_valid, format_obj, specifier);
}

typedef struct Point2DFormatContext {
    bool is_exponential;
    bool is_debug;
    u32 num_sig_figs;
} Point2DFormatContext;

inline static CnxString point2d_format_with_allocator(const CnxFormat* restrict self,
                                                      CnxFormatContext context,
                                                      CnxAllocator allocator) {
    // first we create our interim format string to use as the specifier for formatting
    // x and y
    let context = static_cast(const Point2DFormatContext*)(context.data);
    let exponential = context->is_exponential ? "e" : "";
    let debug = context->is_debug ? "D" : "";
    let num_sig_figs = context->num_sig_figs;
    CnxScopedString specifier = cnx_format_with_allocator("{}{}{}",
                                                          allocator,
                                                          exponential,
                                                          num_sig_figs,
                                                          debug);
    // Then we can forward two copies of the specifier to format to create our format string,
    // which we'll then pass to our actual format call, letting the implementation for f32
    // handle the actual details
    CnxScopedString format_str = cnx_format_with_allocator("Point2D: [x: \{{}\}, y: \{{}\}]",
                                                           allocator,
                                                           specifier,
                                                           specifier);
    let _self = static_cast(const Point2D*)(self.m_self);
    return cnx_format_with_allocator(cnx_string_into_cstring(format_str),
                                     allocator,
                                     _self->x,
                                     _self->y);
}

inline static CnxString point2d_format(const CnxFormat* restrict self,
                                       CnxStringView specifier) {
    return point2d_format_with_allocator(self, specifier, cnx_allocator_new());
}

__attr(maybe_unused) static
ImplTraitFor(CnxFormat, Point2D, point2d_format, point2d_format_with_allocator);

void point2d_print_legacy(const Point2D* restrict self) {
    let formatted = cnx_format("{}", *self);
    printf("%s", cnx_string_into_cstring(formatted));
}

Classes

struct CnxFormat

Enums

enum CnxFormatErrorTypes { CNX_FORMAT_SUCCESS = 0, CNX_FORMAT_BAD_SPECIFIER_INVALID_CHAR_IN_SPECIFIER, CNX_FORMAT_INVALID_CLOSING_BRACE_LOCATION, CNX_FORMAT_UNCLOSED_SPECIFIER, CNX_FORMAT_MORE_SPECIFIERS_THAN_ARGS, CNX_FORMAT_FEWER_SPECIFIERS_THAN_ARGS }
Specifies possible errors that could occur when parsing a format specifier.
enum CnxFormatDefaults { CNX_FORMAT_DEFAULT_NUM_SIG_FIGS = 3 }
Default values for various formatting parameters for Cnx string formatting.

Typedefs

using CnxFormatErrorTypes = enum CnxFormatErrorTypes
Specifies possible errors that could occur when parsing a format specifier.
using CnxFormatDefaults = enum CnxFormatDefaults
Default values for various formatting parameters for Cnx string formatting.

Defines

#define as_format_t(T, x)
Converts the given variable into its associated CnxFormat Trait implementation.
#define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES
Feature enable macro to allow specific user-defined types to be automatically converted to their CnxFormat implementation when passed to cnx_format(format_string, ...) or a similar Cnx string formatting function.
#define CNX_AS_FORMAT_USER_SUPPLIED_TYPES
Define this macro to a comma separated list of conversions to enable specific user-defined types to be automatically converted to their CnxFormat implementation when passed to cnx_format(format_string, ...) or a similar Cnx string formatting function.
#define as_format(x)
Converts the given variable into its associated CnxFormat Trait implementation.
#define cnx_format_with_allocator(format_string, allocator, ...)
Formats the various parameter pack arguments into their associated place in the given format string, using the provided allocator.
#define cnx_format(format_string, ...)
Formats the various parameter pack arguments into their associated place in the given format string, using the default allocator.
#define cnx_vformat_with_allocator(format_string, allocator, num_args, list)
Formats the various va_list parameter pack arguments into their associated place in the given format string, using the provided allocator.
#define cnx_vformat(format_string, num_args, list)
Formats the various va_list parameter pack arguments into their associated place in the given format string, using the default allocator.

Enum documentation

enum CnxFormatErrorTypes

Specifies possible errors that could occur when parsing a format specifier.

Enumerators
CNX_FORMAT_SUCCESS

No error, the specifier is valid.

CNX_FORMAT_BAD_SPECIFIER_INVALID_CHAR_IN_SPECIFIER

An invalid character occurred in the specifier sequence.

CNX_FORMAT_INVALID_CLOSING_BRACE_LOCATION

A closing specifier brace occurred in an invalid location.

CNX_FORMAT_UNCLOSED_SPECIFIER

An unclosed format specifier (ie "{e3") was given.

CNX_FORMAT_MORE_SPECIFIERS_THAN_ARGS

More format specifiers were given in the format string than there were arguments to be formatted.

CNX_FORMAT_FEWER_SPECIFIERS_THAN_ARGS

Fewer format specifiers were given in the format string than there were arguments to be formatted.

enum CnxFormatDefaults

Default values for various formatting parameters for Cnx string formatting.

Enumerators
CNX_FORMAT_DEFAULT_NUM_SIG_FIGS

The default number of significant figures for floating point formatting.

By default, Cnx floating point formatting provides 3 significant figures after the
decimal point in formatted output
@ingroup format 

Typedef documentation

typedef enum CnxFormatErrorTypes CnxFormatErrorTypes

Specifies possible errors that could occur when parsing a format specifier.

typedef enum CnxFormatDefaults CnxFormatDefaults

Default values for various formatting parameters for Cnx string formatting.

Define documentation

#define as_format_t(T, x)

Converts the given variable into its associated CnxFormat Trait implementation.

Parameters
T - The concrete type of x
x - The variable to convert to its CnxFormat Trait implementation
Returns x as CnxFormat

There must be an implementation of CnxFormat for the type T and x must be an lvalue of type T.

#define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES

Feature enable macro to allow specific user-defined types to be automatically converted to their CnxFormat implementation when passed to cnx_format(format_string, ...) or a similar Cnx string formatting function.

Requires that CNX_AS_FORMAT_USER_SUPPLIED_TYPES is also defined to a comma separated list of conversions Example:

#define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES true
#define CNX_AS_FORMAT_USER_SUPPLIED_TYPES T*        : as_format_t(T, x),    \
                                          const T*  : as_format_t(T2, x),   \
                                          T2*       : as_format_t(T2, x),   \
                                          const T2* : as_format_t(T2, x),

#define CNX_AS_FORMAT_USER_SUPPLIED_TYPES

Define this macro to a comma separated list of conversions to enable specific user-defined types to be automatically converted to their CnxFormat implementation when passed to cnx_format(format_string, ...) or a similar Cnx string formatting function.

Requires that CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES is also defined to true Example:

#define CNX_AS_FORMAT_USES_USER_SUPPLIED_TYPES true
#define CNX_AS_FORMAT_USER_SUPPLIED_TYPES T*        : as_format_t(T, x),    \
                                          const T*  : as_format_t(T2, x),   \
                                          T2*       : as_format_t(T2, x),   \
                                          const T2* : as_format_t(T2, x),

#define as_format(x)

Converts the given variable into its associated CnxFormat Trait implementation.

Parameters
x - The variable to convert to its CnxFormat Trait implementation
Returns x as CnxFormat

There must be an implementation of CnxFormat for the type of x and x must be an lvalue

#define cnx_format_with_allocator(format_string, allocator, ...)

Formats the various parameter pack arguments into their associated place in the given format string, using the provided allocator.

Parameters
format_string - The string specifying the format positions, specifiers, and other text that should be present in the output string
allocator - The CnxAllocator to allocate the output string with
... - The parameter pack of arguments to be formatted
Returns The formatted output string

#define cnx_format(format_string, ...)

Formats the various parameter pack arguments into their associated place in the given format string, using the default allocator.

Parameters
format_string - The string specifying the format positions, specifiers, and other text that should be present in the output string
... - The parameter pack of arguments to be formatted
Returns The formatted output string

#define cnx_vformat_with_allocator(format_string, allocator, num_args, list)

Formats the various va_list parameter pack arguments into their associated place in the given format string, using the provided allocator.

Parameters
format_string - The string specifying the format positions, specifiers, and other text that should be present in the output string
allocator - The CnxAllocator to allocate the output string with
num_args - The number of arguments in the parameter pack
list - The va_list parameter pack of arguments to be formatted
Returns The formatted output string

#define cnx_vformat(format_string, num_args, list)

Formats the various va_list parameter pack arguments into their associated place in the given format string, using the default allocator.

Parameters
format_string - The string specifying the format positions, specifiers, and other text that should be present in the output string
num_args - The number of arguments in the parameter pack
list - The va_list parameter pack of arguments to be formatted
Returns The formatted output string