Exess

Installation

This is a brief overview of building and installing from source on the command line using meson. Building requires a relatively modern C compiler (GCC, Clang, and MSVC are tested), meson, and its dependencies Python and (by default) ninja. It can also generate projects for popular IDEs, see the meson configure --backend option for details.

Configuring

The build is configured with the setup command, which creates a new build directory with the given name:

meson setup build

The environment variables CC and CC_LD, if set, give the C compiler and linker during setup, but otherwise it’s best to configure via meson options. After setup, enter the build directory (and stay):

cd build

From here, the build configuration can be inspected:

meson configure

Options can be set by passing C-style “define” options to configure:

meson configure -Dc_args="-march=native" -Dprefix="/opt/mypackage/"

Building

Once configured, the compile command will build everything, and the test command will run all tests:

meson compile
meson test

Installing

A compiled project can be installed with the install command:

meson install

You may need to acquire root permissions to install to a system-wide prefix. For packaging, the installation may be staged to a directory using the DESTDIR environment variable or the meson install --destdir option:

DESTDIR=/tmp/mypackage/ meson install
meson install --destdir=/tmp/mypackage/

Description

Exess is a simple C library for reading and writing values as strings.

It provides portable locale-independent functions for converting common number, date, time, and binary datatypes. Values are preserved exactly wherever possible, so, for example, a float written to a string will be read back as exactly the original value on any system.

The supported datatypes, defined by the XSD specification, are explicitly compatible with many standards (like XML, RDF, and ISO 8601), and most are incidentally compatible with many others (like C and JSON).

The library is straightforward to use and doesn’t depend on an allocator, any other blocking functions, the current locale, or any other shared mutable state, making it safe to use in almost any context.

Supported Datatypes

Most of the XSD datatypes are implemented, omitting some XML-specific and recurring Gregorian calendar datatypes:

  • boolean, like “false”, “true”, “0”, or “1”.

  • decimal, like “1.234”.

  • float and double, like “4.2E1” or “4.2e1”.

  • The unbounded integers integer, nonPositiveInteger, negativeInteger, nonNegativeInteger, and nonPositiveInteger, like “56” or “-78”.

  • The constrained integers long, int, short, byte, unsignedLong, unsignedInt, unsignedShort, and unsignedByte, like “-90” or “12”.

  • duration, like “P1Y2M3DT4H5M”, and its totally-ordered derivatives yearMonthDuration (like “P6Y7M”) and dayTimeDuration (like “P8DT9H”).

  • dateTime, like “2001-01-30T14:30:45”, and its totally-ordered derivative dateTimeStamp (like “1929-10-24T09:30:00-04:00”).

  • time, like “12:30:00.000000001”.

  • date, like “2001-12-31”.

  • hexBinary, like “EC5355”.

  • base64Binary, like “Zm9vYmFy”.

Precision

Time-related datatypes have nanosecond precision.

Precision of decimals and unbounded integers is limited only by the provided buffer size. These arbitrary-precision values are opaque, but can be coerced to simpler datatypes for use.

Dependencies

Exess has no dependencies except for the C standard library, specifically the functions ceil, ldexp, llrint, llrintf, memcmp, memcpy, memset, nextafter, strcmp, strncmp, trunc, and truncf.

Usage

To use exess, the compiler must be configured to add the versioned include directory to the include path, and to link with the corresponding library. The pkg-config package exess-0 describes the required options:

pkg-config --cflags --libs exess-0

The API can then be used by including exess/exess.h:

#include <exess/exess.h>

If package support isn’t available, arguments like -I/usr/include/exess-0 -lexess-0 must be added to the compiler command manually.

Reading Values

Each supported type has a read function that takes a string to read, and a pointer to an output value. It reads the value after skipping any leading whitespace, then returns an ExessResult with a status code and the count of characters read. For example:

int32_t     v = 0;
ExessResult r = exess_read_int("1234", &v);
if (!r.status) {
  printf("Read %zu bytes as %d\n", r.count, v);
}

If there was a syntax error, the status gives the specific problem, and the count gives the offset of the error.

Note that read functions may stop at unrecognized non-whitespace characters and return successfully. This allows values to be read directly from any syntactic context (for example, a quoted field in a document string), but requires the caller to check that the read ended where expected. Proper error detection requires checking the returned count as well, for example:

bool is_null_terminated_int(const char* string) {
  int32_t     v = 0;
  ExessResult r = exess_read_int(string, &v);

  return !r.status && !string[r.count];
}

Writing Values

The corresponding write function takes a value to write, a buffer size in bytes, and a buffer to write to. It returns an ExessResult, with a status code and the count of characters written, not including the trailing null byte.

char        s[12] = {0};
ExessResult r     = exess_write_int(1234, sizeof(s), s);
if (!r.status) {
  printf("Write error: %s\n", exess_strerror(r.status));
}

Allocating Strings

Exess never allocates memory, the calling code is responsible for providing a large enough output buffer.

For datatypes with a bounded length, the array exess_max_lengths, indexed by the ExessDatatype, contains the length of the longest possible string value of that datatype (at most 41), or zero if the datatype is unbounded.

For larger values, the required length can be determined by passing a zero output size to the write function. Then, EXESS_NO_SPACE is returned with the count of characters required. This can be used to precisely allocate memory for the string, taking care to allocate an extra byte for the null terminator. For example:

ExessResult r = exess_write_int(1234, 0, NULL);
char*       s = (char*)calloc(r.count + 1, 1);

r = exess_write_int(1234, r.count + 1, s);

Note that for some types, measuring the output can be about as expensive as actually writing the value. For example, it requires binary to decimal conversion for floating point numbers. So, it’s usually best to avoid measuring twice by writing immediately to a sufficiently large buffer, then copying the result to the final destination.

Generic Values

The fundamental read and write functions all have similar semantics but different type signatures. A generic API that works with opaque buffers is also provided, which can be used to read and write any supported datatype without explicitly handling each case.

Any value can be read with exess_read_value() and written with exess_write_value(), which work similarly to their typed counterparts, except they take a datatype, size, and pointer to a buffer rather than a value. ExessDatatype enumerates all of the supported datatypes.

Unbounded Numbers

There are six unbounded number types: decimal, integer, nonPositiveInteger, negativeInteger, nonNegativeInteger, and positiveInteger. Both exess_read_value() and exess_write_value() support reading and writing a subset of these types, but “big” numbers aren’t supported. Values are stored in the largest corresponding native type: double, int64_t, or uint64_t. If the value doesn’t fit, then exess_read_value() will return an EXESS_OUT_OF_RANGE error.

Canonical Writing

Since values are usually written in canonical form, strings can be converted to canonical form by first reading, then writing again. If the parsed value itself isn’t required, then exess_write_canonical() can be used to do this more efficiently. For example, this will print 12:

char                s[4] = {0};
ExessVariableResult r    = {EXESS_SUCCESS, 0, 0};

r = exess_write_canonical(EXESS_INT, "+12", sizeof(s), s);
if (!r.status) {
  printf("%s\n", s);
}

This is particularly useful for unbounded datatypes, since values are transformed one character at a time, avoiding value conversion, machine limits, and the need for a temporary value buffer.

Exess C API

Symbols

Basic preprocessor symbols and enumerations.

EXESS_XSD_URI

http://www.w3.org/2001/XMLSchema#”

The base URI of XML Schema.

enum ExessOrder

The result of comparing two values.

This follows the usual strcmp() convention, but includes a distinction between comparable values that are strictly less/greater than one another, and incomparable values that are arbitrarily chosen to be less/greater than one another by this implementation.

enumerator EXESS_ORDER_STRICTLY_LESS

Comparable, first is lesser.

enumerator EXESS_ORDER_MAYBE_LESS

Incomparable, first may be lesser.

enumerator EXESS_ORDER_EQUAL

Equal values.

enumerator EXESS_ORDER_MAYBE_GREATER

Incomparable, first may be greater.

enumerator EXESS_ORDER_STRICTLY_GREATER

Comparable, first is greater.

Status

Status codes and return values used for error handling.

Success and various specific errors are reported by an integer status code, which can be converted to a string to produce friendly error messages. Reading and writing functions return a “result”, which has a status code along with a count of bytes read or written.

enum ExessStatus

Status code to describe errors or other relevant situations.

enumerator EXESS_SUCCESS

Success.

enumerator EXESS_LOSS

Lossy operation.

enumerator EXESS_EXPECTED_BOOLEAN

Expected “false”, “true”, “0” or “1”.

enumerator EXESS_EXPECTED_INTEGER

Expected an integer value.

enumerator EXESS_EXPECTED_DURATION

Expected a duration starting with ‘P’.

enumerator EXESS_EXPECTED_SIGN

Expected ‘-’ or ‘+’.

enumerator EXESS_EXPECTED_DIGIT

Expected a digit.

enumerator EXESS_EXPECTED_ZERO

Expected ‘0’.

enumerator EXESS_EXPECTED_COLON

Expected ‘:’.

enumerator EXESS_EXPECTED_DASH

Expected ‘-‘.

enumerator EXESS_EXPECTED_TIME_SEP

Expected ‘T’.

enumerator EXESS_EXPECTED_TIME_TAG

Expected ‘H’, ‘M’, or ‘S’.

enumerator EXESS_EXPECTED_DATE_TAG

Expected ‘Y’, ‘M’, or ‘D’.

enumerator EXESS_EXPECTED_SECOND_TAG

Expected ‘S’.

enumerator EXESS_EXPECTED_HEX

Expected a hexadecimal character.

enumerator EXESS_EXPECTED_BASE64

Expected a base64 character.

enumerator EXESS_BAD_ORDER

Invalid field order.

enumerator EXESS_BAD_VALUE

Invalid value.

enumerator EXESS_OUT_OF_RANGE

Value out of range.

enumerator EXESS_NO_SPACE

Insufficient space.

enumerator EXESS_UNSUPPORTED

Unsupported value.

struct ExessResult

Result returned from a read or write function.

This combines a status code with a byte offset, so it can be used to determine how many characters were read or written, or what error occurred at what character offset.

ExessStatus status

Status code.

size_t count

Number of bytes read or written, excluding null.

struct ExessVariableResult

Result returned from a function that reads and writes.

This is like ExessResult but includes separate read and write counts. This allows the caller to know both how many bytes were read from the input and how many bytes were written to the output.

ExessStatus status

Status code.

size_t read_count

Number of bytes read.

size_t write_count

Number of bytes written, excluding null.

const char *exess_strerror(ExessStatus status)

Return a string describing a status code in plain language.

The returned string is always one sentence, with an uppercase first character, and no trailing period.

Fixed Type API

Statically type-safe API for working with values of a known fixed type.

Numbers

decimal

A decimal is a decimal number of arbitrary precision, but this implementation only supports values that fit in a double.

Unlike double, decimal is written in numeric form, never in scientific notation. Special infinity and NaN values aren’t supported. Note that the decimal representation for some numbers is very long, so double may be a better choice for values in a wide range.

Canonical form has no leading “+” sign, and at most 1 leading or trailing zero such that there is at least 1 digit on either side of the decimal point, like “12.34”, “-1.0”, and “0.0”.

Non-canonical form allows a leading “+”, any number of leading and trailing zeros, any number of digits (including zero) on either side of the point, and doesn’t require a decimal point, like “+1”, “01”, “-.5”, “4.”, and “42”.

ExessVariableResult exess_read_decimal(const char *str, size_t out_size, void *out)

Read a decimal string after any leading whitespace.

The high-precision value format is opaque to the user, but can be written or converted to other types.

Parameters:
  • str – String to read.

  • out_size – Size of out in bytes.

  • out – Parsed value.

Returns:

The read_count of characters read, write_count of bytes written, and a status. The status will be ExessStatus.EXESS_LOSS if an approximate value was successfully read by dropping significant digits.

ExessResult exess_write_decimal(size_t value_size, const void *value, size_t buf_size, char *buf)

Write a canonical decimal string.

Parameters:
  • value_size – The size of value in bytes.

  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

integer

An integer is a decimal with no decimal point or fractional component.

ExessVariableResult exess_read_integer(const char *str, size_t out_size, void *out)

Read a integer string after any leading whitespace.

The high-precision value format is opaque to the user, but can be written or converted to other types.

Parameters:
  • str – String to read.

  • out_size – Size of out in bytes.

  • out – Parsed value.

Returns:

The read_count of characters read, write_count of bytes written, and a status. The status will be ExessStatus.EXESS_LOSS if an approximate value was successfully read by dropping significant digits.

ExessResult exess_write_integer(size_t value_size, const void *value, size_t buf_size, char *buf)

Write a canonical integer string.

Parameters:
  • value_size – The size of value in bytes.

  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

double

A double is an IEEE-754 64-bit floating point number, written in scientific notation.

Canonical form has no leading “+” sign, at most 1 leading or trailing zero such that there is at least 1 digit on either side of the decimal point, and always has an exponent, like “12.34E56”, “-1.0E-2”, and “-0.0E0”. The special values negative infinity, positive infinity, and not-a-number are written “-INF”, “INF”, and “NaN”, respectively.

Non-canonical form allows a leading “+”, any number of leading and trailing zeros, any number of digits (including zero) on either side of the point, and doesn’t require an exponent or decimal point, like “+1E3”, “1E+3”, “.5E3”, “4.2”, and “42”.

EXESS_MAX_DOUBLE_LENGTH

24U

The maximum length of a canonical double string.

ExessResult exess_read_double(const char *str, double *out)

Read a double string after any leading whitespace.

Values too large (in absolute terms) to store will produce infinity, values too small will produce zero, and insignificant digits may be discarded. Note that these cases are considered successful since the value space of double has limited precision by definition.

Parameters:
  • str – String to read.

  • out – Parsed value, or NaN on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_double(double value, size_t buf_size, char *buf)

Write a canonical double string.

Any double value is supported. Reading the resulting string with exess_read_double() will produce exactly value, except the extra bits in NaNs aren’t preserved.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

float

A float is an IEEE-754 32-bit floating point number, written in scientific notation.

The lexical form is the same as double, but the value space has less precision.

EXESS_MAX_FLOAT_LENGTH

15U

The maximum length of a canonical float string.

ExessResult exess_read_float(const char *str, float *out)

Read a float string after any leading whitespace.

Values too large (in absolute terms) to store will produce infinity, values too small will produce zero, and insignificant digits may be discarded. Note that these cases are considered successful since the value space of float has limited precision by definition.

Parameters:
  • str – String to read.

  • out – Parsed value, or NaN on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_float(float value, size_t buf_size, char *buf)

Write a canonical float string.

Any float value is supported. Reading the resulting string with exess_read_float() will produce exactly value, except the extra bits in NaNs aren’t preserved.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

boolean

A boolean has only two possible values, canonically written as “false” and “true”.

The non-canonical forms “0” and “1” are also supported.

EXESS_MAX_BOOLEAN_LENGTH

5U

The maximum length of a canonical boolean string.

ExessResult exess_read_boolean(const char *str, bool *out)

Read a boolean string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or false on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_boolean(bool value, size_t buf_size, char *buf)

Write a canonical boolean string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

long

A long is a signed 64-bit integer, written in decimal.

Values range from -9223372036854775808 to 9223372036854775807 inclusive.

Canonical form has no leading “+” sign and no leading zeros (except for the number “0”), like “-1”, “0”, and “1234”.

Non-canonical form allows a leading “+” and any number of leading zeros, like “01” and “+0001234”.

EXESS_MAX_LONG_LENGTH

20U

The maximum length of a canonical long string.

ExessResult exess_read_long(const char *str, int64_t *out)

Read a long string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_long(int64_t value, size_t buf_size, char *buf)

Write a canonical long string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

int

An int is a signed 32-bit integer.

The lexical form is the same as long, but values range from -2147483648 to 2147483647 inclusive.

EXESS_MAX_INT_LENGTH

11U

The maximum length of a canonical int string.

ExessResult exess_read_int(const char *str, int32_t *out)

Read an int string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_int(int32_t value, size_t buf_size, char *buf)

Write a canonical int string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

short

A short is a signed 16-bit integer.

The lexical form is the same as long, but values range from -32768 to 32767 inclusive.

EXESS_MAX_SHORT_LENGTH

6U

The maximum length of a canonical short string.

ExessResult exess_read_short(const char *str, int16_t *out)

Read a short string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_short(int16_t value, size_t buf_size, char *buf)

Write a canonical short string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

byte

A byte is a signed 8-bit integer.

The lexical form is the same as long, but values range from -128 to 127 inclusive.

EXESS_MAX_BYTE_LENGTH

4U

The maximum length of a canonical byte string.

ExessResult exess_read_byte(const char *str, int8_t *out)

Read a byte string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_byte(int8_t value, size_t buf_size, char *buf)

Write a canonical byte string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

unsignedLong

An unsignedLong is an unsigned 64-bit integer, written in decimal.

Values range from 0 to 18446744073709551615 inclusive.

Canonical form has no leading “+” sign and no leading zeros (except for the number “0”), like “0”, and “1234”.

Non-canonical form allows a leading “+”, a leading “-” for zero, and any number of leading zeros, like “01” and “0001234”.

EXESS_MAX_ULONG_LENGTH

20U

The maximum length of a canonical unsignedLong string.

ExessResult exess_read_ulong(const char *str, uint64_t *out)

Read an unsignedLong string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_ulong(uint64_t value, size_t buf_size, char *buf)

Write a canonical unsignedLong string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

unsignedInt

An unsignedInt is an unsigned 32-bit integer.

The lexical form is the same as unsignedLong, but values range from 0 to 4294967295 inclusive.

EXESS_MAX_UINT_LENGTH

10U

The maximum length of a canonical unsignedInt string.

ExessResult exess_read_uint(const char *str, uint32_t *out)

Read an unsignedInt string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_uint(uint32_t value, size_t buf_size, char *buf)

Write a canonical unsignedInt string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

unsignedShort

An unsignedShort is an unsigned 16-bit integer.

The lexical form is the same as unsignedLong, but values range from 0 to 65535 inclusive.

EXESS_MAX_USHORT_LENGTH

5U

The maximum length of a canonical unsignedShort string.

ExessResult exess_read_ushort(const char *str, uint16_t *out)

Read an unsignedShort string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_ushort(uint16_t value, size_t buf_size, char *buf)

Write a canonical unsignedShort string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

unsignedByte

An unsignedByte is an unsigned 8-bit integer.

The lexical form is the same as unsignedLong, but values range from 0 to 255 inclusive.

EXESS_MAX_UBYTE_LENGTH

3U

The maximum length of a canonical unsignedByte string.

ExessResult exess_read_ubyte(const char *str, uint8_t *out)

Read an unsignedByte string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_ubyte(uint8_t value, size_t buf_size, char *buf)

Write a canonical unsignedByte string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

Time and Date

Timezone Offsets

Some time and date values can have a timezone qualifier suffix.

A timezone isn’t a datatype unto itself, but only exists as a part of another value.

Canonical form starts with a sign, followed by two-digit hour and minute offsets separated by a colon, like “-06:00” and “+02:30”. The zero offset, UTC, is written “Z”.

Non-canonical form also allows writing UTC as “-00:00” or “+00:00”.

This implementation only supports a resolution of 15 minutes, that is, only offsets at 0, 15, 30, and 45 minutes within an hour.

EXESS_TIMEZONE_LOCAL

((ExessTimezone)127U)

Sentinel value for local time.

EXESS_TIMEZONE_UTC

((ExessTimezone)0U)

Sentinel value for UTC time.

typedef int8_t ExessTimezone

A time zone offset in quarter hours.

This is stored in a single byte for compactness in other structures. Valid values are from -56 to 56 inclusive.

ExessTimezone exess_timezone(int8_t hours, int8_t minutes)

Construct a time zone offset from hours and minutes.

This is a convenience constructor that handles the conversion from hours and minutes to the quarter-hour offset used in exess. The sign of both values must be the same. Hours can be from -14 to 14 inclusive, and minutes can only be -45, -30, -15, 0, 15, 30, or 45.

Returns:

A time zone offset in quarter hours, or EXESS_TIMEZONE_LOCAL if the parameters are invalid or not supported.

duration

A duration is a positive or negative duration of time, written in ISO 8601 format like “PnYnMnDTnHnMnS” where each “n” is a number and fields may be omitted if they are zero.

All numbers must be integers, except for seconds which may be a decimal. If seconds is a decimal, then at least one digit must follow the decimal point. A negative duration is written with “-” as the first character, for example “-P60D”.

Canonical form omits all zero fields and writes no leading or trailing zeros, except for the zero duration which is written “P0Y”, for example “P1DT2H”, “PT30M”, or “PT4.5S”.

Non-canonical form allows zero fields, leading zeros, and for seconds to be written as a decimal even if it’s integer, for example “P06D”, “PT7.0S”, or “P0Y0M01DT06H00M00S”.

EXESS_MAX_DURATION_LENGTH

41U

The maximum length of a duration string from exess_write_duration()

struct ExessDuration

Duration of time.

To save space and to simplify arithmetic, this representation only stores two values: integer months, and decimal seconds (to nanosecond precision). These values are converted to and from the other fields during writing and reading. Years and months are stored as months, and days, hours, minutes, and seconds are stored as seconds.

The sign of all members must match, so a negative duration has all non-positive members, and a positive duration has all non-negative members.

int32_t months

Number of months.

int32_t seconds

Number of seconds.

int32_t nanoseconds

Number of nanoseconds.

ExessOrder exess_compare_duration(ExessDuration lhs, ExessDuration rhs)

Compare two durations.

Loosely speaking, a duration is considered less than another if it’s a shorter duration of time. However, note that two durations may not be comparable, since the relation between fields, such as the number of days in a month, varies at different times. Strictly speaking, two durations are comparable if adding them to any dateTime would produce results with the same ordering.

The ExessDuration representation condenses all fields into two values: months and seconds. When the two values are incomparable, a month is considered greater than any number of seconds.

Values with only months, or only seconds, are always comparable. Applications are encouraged to always use such values and never carry seconds into months or vice-versa.

Returns:

Less than, equal to, or greater than zero if lhs is less than, equal to, or greater than rhs, respectively. Comparable and incomparable cases may also be distinguished, see ExessOrder for details.

ExessResult exess_read_duration(const char *str, ExessDuration *out)

Read a duration string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_duration(ExessDuration value, size_t buf_size, char *buf)

Write a canonical duration string.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

ExessStatus.EXESS_SUCCESS on success, ExessStatus.EXESS_NO_SPACE if the buffer is too small, or ExessStatus.EXESS_BAD_VALUE if the value is invalid.

dateTime

A dateTime is a date and time, with an optional timezone offset.

Strings have the form YYYY-MM-DDTHH:MM:SS with an optional timezone suffix, at least 4 year digits (negative or positive), and all other fields positive two-digit integers except seconds which may be a decimal. For example, “2001-02-03T12:13:14.56” or “2001-02-03T12:13:14.56-06:00”

Canonical form only includes a decimal point if the number of seconds isn’t an integer. This implementation supports up to nanosecond resolution. Midnight at the end of the day, like “1999-12-31T24:00:00” is a valid value, equivalent to the start of the next day, like “2000-01-01T00:00:00”, which is the canonical form.

EXESS_MAX_DATE_TIME_LENGTH

37U

The maximum length of a dateTime string from exess_write_date_time()

struct ExessDateTime

Date and time.

This representation follows the syntax, except the timezone offset is stored between the date and time for more efficient packing.

int16_t year

Year: any positive or negative value.

uint8_t month

Month: [1, 12].

uint8_t day

Day: [1, 31].

ExessTimezone zone

Timezone offset in quarter hours.

uint8_t hour

Hour: [0, 24].

uint8_t minute

Minute: [0, 59].

uint8_t second

Second: [0, 59].

uint32_t nanosecond

Nanosecond: [0, 999999999].

ExessDateTime exess_add_date_time_duration(ExessDateTime s, ExessDuration d)

Add a duration to a dateTime.

This advances or rewinds by the given duration, depending on whether the duration is positive or negative.

If underflow or overflow occur, then this will return an infinite value. A positive infinity has all fields at maximum, and a negative infinity has all fields at minimum, except zone which is preserved from the input (so infinities are comparable with the values they came from). Since 0 and 255 are never valid months, these can be tested for by checking if the year and month are INT16_MIN and 0, or INT16_MAX and INT8_MAX.

Returns:

s + d, or an infinite past or infinite future if underflow or overflow occurs.

ExessOrder exess_compare_date_time(ExessDateTime lhs, ExessDateTime rhs)

Compare two dateTimes.

Note that local and zoned dateTimes times may not be comparable. When the values aren’t comparable, this function arbitrarily chooses the local time to be lesser.

Returns:

Less than, equal to, or greater than zero if lhs is less than, equal to, or greater than rhs, respectively. Comparable and incomparable cases may also be distinguished, see ExessOrder for details.

ExessDateTime exess_date_time_to_utc(ExessDateTime datetime)

Convert a dateTime to UTC.

Returns:

The input converted to UTC if it has a timezone, otherwise the unmodified input.

ExessResult exess_read_date_time(const char *str, ExessDateTime *out)

Read a dateTime string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_date_time(ExessDateTime value, size_t buf_size, char *buf)

Write a dateTime string.

The written string is in canonical form, except that midnight at the end of the day (24:00:00) is written as-is. Note that this differs from the other write functions, which always write canonical form.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

ExessStatus.EXESS_SUCCESS on success, ExessStatus.EXESS_NO_SPACE if the buffer is too small, or ExessStatus.EXESS_BAD_VALUE if the value is invalid.

date

A date is a year, month, and day, with an optional timezone offset.

Strings have the form YYYY-MM-DD with an optional timezone suffix, at least 4 year digits (negative or positive), and exactly 2 digits for both month and day. For example, “2001-02-03” or “2001-02-03-06:00”.

Canonical form has no leading zeros for the year.

EXESS_MAX_DATE_LENGTH

18U

The maximum length of a date string from exess_write_date()

struct ExessDate

Date.

int16_t year

Year.

uint8_t month

Month: [1, 12].

uint8_t day

Day: [1, 31].

ExessTimezone zone

Timezone offset in quarter hours.

ExessOrder exess_compare_date(ExessDate lhs, ExessDate rhs)

Compare two dates.

Note that local and zoned dates may not be comparable. See exess_compare_date_time() for details.

Returns:

Less than, equal to, or greater than zero if lhs is less than, equal to, or greater than rhs, respectively. Comparable and incomparable cases may also be distinguished, see ExessOrder for details.

ExessResult exess_read_date(const char *str, ExessDate *out)

Read a date string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_date(ExessDate value, size_t buf_size, char *buf)

Write a canonical date string.

The output is always in canonical form, like 2001-04-12 or -2001-10-26+02:00.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

ExessStatus.EXESS_SUCCESS on success, ExessStatus.EXESS_NO_SPACE if the buffer is too small, or ExessStatus.EXESS_BAD_VALUE if the value is invalid.

time

A time is a time of day, with an optional timezone offset.

Strings have the form HH:MM:SS with an optional timezone suffix, where seconds may be a decimal value, for example, “12:13:14”, “12:13:14.56”, or “12:13:14.56-07:00”.

Canonical form only includes a decimal point if the number of seconds isn’t an integer. This implementation supports up to nanosecond resolution. Note that midnight at the end of the day (like “24:00:00”) is a valid lexical form, but unlike dateTime, 24 isn’t a valid hour value. Such forms will be read as 00:00:00.

EXESS_MAX_TIME_LENGTH

24U

The maximum length of a time string from exess_write_time()

struct ExessTime

Time.

ExessTimezone zone

Timezone offset in quarter hours.

uint8_t hour

Hour: [0, 23].

uint8_t minute

Minute: [0, 59].

uint8_t second

Second: [0, 59].

uint32_t nanosecond

Nanosecond: [0, 999999999].

ExessOrder exess_compare_time(ExessTime lhs, ExessTime rhs)

Compare two times.

Note that local and zoned times may not be comparable. See exess_compare_date_time() for details.

Returns:

Less than, equal to, or greater than zero if lhs is less than, equal to, or greater than rhs, respectively. Comparable and incomparable cases may also be distinguished, see ExessOrder for details.

ExessResult exess_read_time(const char *str, ExessTime *out)

Read a time string after any leading whitespace.

Parameters:
  • str – String to read.

  • out – Parsed value, or zero on error.

Returns:

The count of characters read, and a status.

ExessResult exess_write_time(ExessTime value, size_t buf_size, char *buf)

Write a canonical time string.

The output is always in canonical form, like “12:15” or “02:00Z”.

Parameters:
  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

ExessStatus.EXESS_SUCCESS on success, ExessStatus.EXESS_NO_SPACE if the buffer is too small, or ExessStatus.EXESS_BAD_VALUE if the value is invalid.

Binary

base64Binary

A base64Binary is arbitrary binary data in base64 encoding.

Strings consist of characters from the base64 alphabet ([0-9], [A-Z], [a-z], “+”, “/”, and “=”) with whitespace (tab, newline, carriage return, or space) allowed anywhere. Strings are padded to a multiple of four non-whitespace characters with with trailing “=” characters. For example, the base64 encoding of the string “data” is “ZGF0YQo=”.

Canonical form contains no whitespace.

size_t exess_decoded_base64_size(size_t length)

Return the maximum number of bytes required to decode length bytes of base64.

The returned value is an upper bound which is only exact for canonical strings.

Parameters:
  • length – Number of input (text) bytes to decode.

Returns:

The maximum size of a decoded value in bytes.

ExessVariableResult exess_read_base64(const char *str, size_t out_size, void *out)

Read a binary value from a base64 string.

Canonical syntax is a multiple of 4 base64 characters, with either 1 or 2 trailing “=” characters as necessary, like “Zm9vYg==”, with no whitespace. All whitespace is skipped when reading.

The caller must allocate a large enough buffer to read the value, otherwise an ExessStatus.EXESS_NO_SPACE error will be returned. The required space can be calculated with exess_decoded_base64_size().

When this is called, out must point to a buffer of at least out_size bytes. The returned result contains the exact size of the decoded data, which may be smaller than out_size. Only these first bytes are written, the rest of the buffer isn’t modified.

Parameters:
  • str – String to read.

  • out_size – Size of out in bytes.

  • out – Decoded binary data.

Returns:

The read_count of characters read, write_count of bytes written, and a status.

ExessResult exess_write_base64(size_t data_size, const void *data, size_t buf_size, char *buf)

Write a canonical base64Binary string.

The data is always written in canonical form, as a multiple of 4 characters with no whitespace and 1 or 2 trailing “=” characters as padding if necessary.

Parameters:
  • data_size – Size of data in bytes.

  • data – Data to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

hexBinary

A hexBinary is arbitrary binary data in hexadecimal encoding.

Strings consist of an even number of hexadecimal characters ([0-9], [A-F], and [a-f]). Note that, unlike base64Binary, whitespace between characters isn’t allowed.

size_t exess_decoded_hex_size(size_t length)

Return the maximum number of bytes required to decode length bytes of hex.

The returned value is an upper bound which is only exact for canonical strings.

Parameters:
  • length – Number of input (text) bytes to decode.

Returns:

The size of a decoded value in bytes.

ExessVariableResult exess_read_hex(const char *str, size_t out_size, void *out)

Read a binary value from a hex string.

Canonical syntax is an even number of uppercase hexadecimal digits with no whitespace, like “666F6F”. Lowercase hexadecimal is also supported, and all whitespace is skipped when reading.

The caller must allocate a large enough buffer to read the value, otherwise an ExessStatus.EXESS_NO_SPACE error will be returned. The required space can be calculated with exess_decoded_hex_size().

When this is called, out must point to a buffer of at least out_size bytes. The returned result contains the exact size of the decoded data, which may be smaller than out_size. Only these first bytes are written, the rest of the buffer isn’t modified.

Parameters:
  • str – String to parse.

  • out_size – Size of out in bytes.

  • out – Decoded binary data.

Returns:

The read_count of characters read, write_count of bytes written, and a status.

ExessResult exess_write_hex(size_t data_size, const void *data, size_t buf_size, char *buf)

Write a canonical hexBinary string.

The data is always written in canonical form, as an even number of uppercase hexadecimal digits with no whitespace.

Parameters:
  • data_size – Size of data in bytes.

  • data – Data to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

Dynamic Type API

API for working with general values of a type given at runtime.

Datatypes

Integer identifiers for datatypes, and utility functions to access their properties.

EXESS_MAX_DATATYPE

26U

The unsigned value of the largest ExessDatatype enumerator.

enum ExessDatatype

A supported datatype, or zero.

enumerator EXESS_NOTHING

Sentinel for unknown datatypes or errors.

enumerator EXESS_BOOLEAN

boolean

enumerator EXESS_DOUBLE

double

enumerator EXESS_FLOAT

float

enumerator EXESS_LONG

long

enumerator EXESS_INT

int

enumerator EXESS_SHORT

short

enumerator EXESS_BYTE

byte

enumerator EXESS_ULONG

unsignedLong

enumerator EXESS_UINT

unsignedInt

enumerator EXESS_USHORT

unsignedShort

enumerator EXESS_UBYTE

unsignedByte

enumerator EXESS_DECIMAL

decimal

enumerator EXESS_INTEGER

integer

enumerator EXESS_NON_POSITIVE_INTEGER

integer <= 0

enumerator EXESS_NEGATIVE_INTEGER

integer < 0

enumerator EXESS_NON_NEGATIVE_INTEGER

integer >= 0

enumerator EXESS_POSITIVE_INTEGER

integer > 0

enumerator EXESS_DURATION

duration

enumerator EXESS_YEAR_MONTH_DURATION

duration

enumerator EXESS_DAY_TIME_DURATION

duration

enumerator EXESS_DATE_TIME

dateTime

enumerator EXESS_DATE_TIME_STAMP

dateTime

enumerator EXESS_TIME

time

enumerator EXESS_DATE

date

enumerator EXESS_HEX

hexBinary

enumerator EXESS_BASE64

base64Binary

ExessDatatype exess_datatype_from_name(const char *name)

Return the datatype tag for a datatype name.

Parameters:
  • name – The camel-case name of a supported datatype (its URI fragment), like “unsignedLong” or “dateTime”.

Returns:

A datatype tag, or ExessDatatype.EXESS_NOTHING if no such datatype is known.

ExessDatatype exess_datatype_from_uri(const char *uri)

Return the datatype tag for a datatype URI.

Parameters:
Returns:

A datatype tag, or ExessDatatype.EXESS_NOTHING if no such datatype is known.

const char *exess_datatype_name(ExessDatatype datatype)

Return the name of a datatype, or null.

This returns the suffix of the URI that would be returned by exess_datatype_uri().

Parameters:
  • datatype – Datatype tag.

Returns:

The name of the datatype, or null for ExessDatatype.EXESS_NOTHING.

const char *exess_datatype_uri(ExessDatatype datatype)

Return the URI for a datatype, or null.

This only returns URIs that start with “http://www.w3.org/2001/XMLSchema#”.

Parameters:
  • datatype – Datatype tag.

Returns:

The URI of the datatype, or null for ExessDatatype.EXESS_NOTHING.

uint8_t exess_max_lengths[EXESS_MAX_DATATYPE + 1U]

Array of maximum string lengths, indexed by datatype.

Each entry is the maximum length of a string with the indexed datatype, or zero if the datatype is unknown or unbounded. For example:

unsigned four_bytes = exess_max_lengths[EXESS_BYTE];

The unbounded types are ExessDatatype.EXESS_DECIMAL, ExessDatatype.EXESS_INTEGER and its half-bounded subtypes ExessDatatype.EXESS_NON_POSITIVE_INTEGER, ExessDatatype.EXESS_NEGATIVE_INTEGER, ExessDatatype.EXESS_NON_NEGATIVE_INTEGER, and ExessDatatype.EXESS_POSITIVE_INTEGER, and the binary types ExessDatatype.EXESS_HEX and ExessDatatype.EXESS_BASE64.

uint8_t exess_value_sizes[EXESS_MAX_DATATYPE + 1U]

Array of binary value sizes, indexed by datatype.

Each entry is the size of the binary representation of the indexed datatype (not a string length), or zero for values that can be arbitrarily large.

Generic Values

A generic interface for reading and writing binary values.

EXESS_ALIGN

Attribute to align a buffer for any supported value.

EXESS_MAX_FIXED_SIZE

12U

The maximum size of a supported fixed-size value in bytes.

ExessOrder exess_compare_value(ExessDatatype lhs_datatype, size_t lhs_size, const void *lhs_value, ExessDatatype rhs_datatype, size_t rhs_size, const void *rhs_value)

Compare two values.

Returns:

Less than, equal to, or greater than zero if the left-hand value is less than, equal to, or greater than the right-hand value, respectively. Comparable and incomparable cases may also be distinguished, see ExessOrder for details.

ExessVariableResult exess_read_value(ExessDatatype datatype, const char *str, size_t out_size, void *out)

Read any supported datatype from a string.

Note that out must be suitably aligned for the datatype being read, it will be dereferenced directly as a pointer to the value type. A 64-bit aligned buffer will be suitably aligned for any datatype.

Parameters:
  • str – String to read.

  • datatype – Datatype to read the string as.

  • out_size – Size of out in bytes.

  • out – Parsed value on success.

Returns:

The read_count from str, write_count to out (both in bytes), and a status.

ExessResult exess_write_value(ExessDatatype datatype, size_t value_size, const void *value, size_t buf_size, char *buf)

Write any supported datatype to a canonical string.

Note that value must be suitably aligned for the datatype being written, it will be dereferenced directly as a pointer to the value type. A 64-bit aligned buffer will be suitably aligned for any datatype.

Parameters:
  • datatype – Datatype of value.

  • value_size – Size of value in bytes.

  • value – Value to write.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The count of characters in the output, and a status (ExessStatus.EXESS_SUCCESS, or ExessStatus.EXESS_NO_SPACE if the buffer is too small).

Canonical Form

Rewriting generic strings in canonical form.

ExessVariableResult exess_write_canonical(ExessDatatype datatype, const char *str, size_t buf_size, char *buf)

Rewrite a supported datatype in canonical form.

Parameters:
  • datatype – Datatype of value.

  • str – Input value string.

  • buf_size – Size of buf in bytes.

  • buf – Output buffer, or null to only measure.

Returns:

The read_count from str, write_count to buf (both in bytes), and a status. The status may be an error from reading or writing.

Coercion

Coercing values to different datatypes.

typedef uint32_t ExessCoercions

Bitwise OR of ExessCoercion flags.

If this is zero, then only lossless coercions will be performed. A lossless coercion is when the value has been perfectly preserved in the target datatype, and coercing it back will result in the same value.

For some datatype combinations this will always be the case, for example from short to long. For others it will depend on the value, for example only the numbers 0 and 1 coerce to boolean without loss.

enum ExessCoercion

Coercion flags.

These values are ORed together to enable different kinds of lossy conversion.

enumerator EXESS_APPROXIMATE

Allow coercions that reduce the precision of values.

This allows coercions that are lossy only in terms of precision, so the resulting value is approximately equal to the original value. Specifically, this allows coercing double to float.

enumerator EXESS_ROUND

Allow coercions that round to the nearest integer.

This allows coercing floating point numbers to integers by rounding to the nearest integer, with halfway cases rounding towards even (the default IEEE-754 rounding order).

enumerator EXESS_TRUNCATE

Allow coercions that truncate significant parts of values.

Specifically, this allows coercing any number to boolean, and dateTime to date or time.

ExessResult exess_coerce_value(ExessCoercions coercions, ExessDatatype in_datatype, size_t in_size, const void *in, ExessDatatype out_datatype, size_t out_size, void *out)

Coerce a value to another datatype if possible.

Parameters:
  • coercions – Enabled coercion flags. If this is zero, then ExessStatus.EXESS_SUCCESS is only returned if the resulting value can be coerced back to the original type without any loss of data. Otherwise, the lossy coercions enabled by the set bits will be attempted.

  • in_datatype – Datatype of in.

  • in_size – Size of in in bytes.

  • in – Input value to coerce.

  • out_datatype – Datatype to convert to.

  • out_size – Size of out in bytes.

  • out – Coerced value on success.

Returns:

The count of bytes written, and a status: ExessStatus.EXESS_SUCCESS on successful conversion, ExessStatus.EXESS_OUT_OF_RANGE if the value is outside the range of the target type, ExessStatus.EXESS_LOSS if the required coercion isn’t enabled, or ExessStatus.EXESS_UNSUPPORTED if conversion between the types isn’t supported at all.