aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/exess/src
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/exess/src')
-rw-r--r--subprojects/exess/src/.clang-tidy12
-rw-r--r--subprojects/exess/src/attributes.h30
-rw-r--r--subprojects/exess/src/base64.c163
-rw-r--r--subprojects/exess/src/bigint.c605
-rw-r--r--subprojects/exess/src/bigint.h118
-rw-r--r--subprojects/exess/src/boolean.c69
-rw-r--r--subprojects/exess/src/byte.c45
-rw-r--r--subprojects/exess/src/canonical.c309
-rw-r--r--subprojects/exess/src/coerce.c422
-rw-r--r--subprojects/exess/src/datatype.c79
-rw-r--r--subprojects/exess/src/date.c112
-rw-r--r--subprojects/exess/src/date_utils.h65
-rw-r--r--subprojects/exess/src/datetime.c265
-rw-r--r--subprojects/exess/src/decimal.c267
-rw-r--r--subprojects/exess/src/decimal.h63
-rw-r--r--subprojects/exess/src/digits.c243
-rw-r--r--subprojects/exess/src/digits.h38
-rw-r--r--subprojects/exess/src/double.c53
-rw-r--r--subprojects/exess/src/duration.c322
-rw-r--r--subprojects/exess/src/exess_config.h79
-rw-r--r--subprojects/exess/src/float.c44
-rw-r--r--subprojects/exess/src/hex.c131
-rw-r--r--subprojects/exess/src/ieee_float.h63
-rw-r--r--subprojects/exess/src/int.c45
-rw-r--r--subprojects/exess/src/int_math.c78
-rw-r--r--subprojects/exess/src/int_math.h72
-rw-r--r--subprojects/exess/src/long.c110
-rw-r--r--subprojects/exess/src/macros.h31
-rw-r--r--subprojects/exess/src/read_utils.c51
-rw-r--r--subprojects/exess/src/read_utils.h77
-rw-r--r--subprojects/exess/src/scientific.c125
-rw-r--r--subprojects/exess/src/scientific.h35
-rw-r--r--subprojects/exess/src/short.c45
-rw-r--r--subprojects/exess/src/soft_float.c161
-rw-r--r--subprojects/exess/src/soft_float.h71
-rw-r--r--subprojects/exess/src/strerror.c70
-rw-r--r--subprojects/exess/src/string_utils.h74
-rw-r--r--subprojects/exess/src/strtod.c405
-rw-r--r--subprojects/exess/src/strtod.h33
-rw-r--r--subprojects/exess/src/time.c172
-rw-r--r--subprojects/exess/src/time_utils.h35
-rw-r--r--subprojects/exess/src/timezone.c150
-rw-r--r--subprojects/exess/src/timezone.h59
-rw-r--r--subprojects/exess/src/ubyte.c45
-rw-r--r--subprojects/exess/src/uint.c45
-rw-r--r--subprojects/exess/src/ulong.c94
-rw-r--r--subprojects/exess/src/ushort.c45
-rw-r--r--subprojects/exess/src/variant.c431
-rw-r--r--subprojects/exess/src/warnings.h46
-rw-r--r--subprojects/exess/src/write_utils.c50
-rw-r--r--subprojects/exess/src/write_utils.h85
-rw-r--r--subprojects/exess/src/year.c98
52 files changed, 6435 insertions, 0 deletions
diff --git a/subprojects/exess/src/.clang-tidy b/subprojects/exess/src/.clang-tidy
new file mode 100644
index 00000000..8023398e
--- /dev/null
+++ b/subprojects/exess/src/.clang-tidy
@@ -0,0 +1,12 @@
+Checks: >
+ *,
+ -*-magic-numbers,
+ -*-uppercase-literal-suffix,
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
+ -hicpp-multiway-paths-covered,
+ -llvm-header-guard,
+ -llvmlibc-*,
+ -misc-no-recursion,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/subprojects/exess/src/attributes.h b/subprojects/exess/src/attributes.h
new file mode 100644
index 00000000..1575113b
--- /dev/null
+++ b/subprojects/exess/src/attributes.h
@@ -0,0 +1,30 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_ATTRIBUTES_H
+#define EXESS_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define EXESS_I_PURE_FUNC __attribute__((pure))
+# define EXESS_I_CONST_FUNC __attribute__((const))
+# define EXESS_I_MALLOC_FUNC __attribute__((malloc))
+#else
+# define EXESS_I_PURE_FUNC
+# define EXESS_I_CONST_FUNC
+# define EXESS_I_MALLOC_FUNC
+#endif
+
+#endif // EXESS_ATTRIBUTES_H
diff --git a/subprojects/exess/src/base64.c b/subprojects/exess/src/base64.c
new file mode 100644
index 00000000..de64ee68
--- /dev/null
+++ b/subprojects/exess/src/base64.c
@@ -0,0 +1,163 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "macros.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <string.h>
+
+// Map a 6-bit base64 group to a base64 digit
+static inline uint8_t
+map(const unsigned group)
+{
+ assert(group < 64);
+
+ // See <http://tools.ietf.org/html/rfc3548#section-3>.
+ static const uint8_t b64_map[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ return b64_map[group];
+}
+
+// Unmap a base64 digit to the numeric value used for decoding
+static inline uint8_t
+unmap(const uint8_t in)
+{
+ /* Table indexed by encoded characters that contains the numeric value used
+ for decoding, shifted up by 47 to be in the range of printable ASCII. A
+ '$' is a placeholder for characters not in the base64 alphabet. */
+ static const uint8_t b64_unmap[] =
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$m$$$ncdefghijkl$$$$$$"
+ "$/0123456789:;<=>?@ABCDEFGH$$$$$$IJKLMNOPQRSTUVWXYZ[\\]^_`ab$$$$"
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$";
+
+ return (uint8_t)(b64_unmap[in] - 47u);
+}
+
+static char
+next_char(const char* const str, size_t* const i)
+{
+ *i += skip_whitespace(str + *i);
+
+ return str[*i];
+}
+
+size_t
+exess_base64_decoded_size(const size_t length)
+{
+ return (length * 3) / 4 + 2;
+}
+
+ExessResult
+exess_read_base64(ExessBlob* const out, const char* const str)
+{
+ uint8_t* const uout = (uint8_t*)out->data;
+ const uint8_t* const ustr = (const uint8_t*)str;
+ size_t i = 0u;
+ size_t o = 0u;
+
+ while (str[i]) {
+ // Skip leading whitespace
+ i += skip_whitespace(str + i);
+ if (!str[i]) {
+ break;
+ }
+
+ // Read next chunk of 4 input characters
+ uint8_t in[] = "====";
+ for (size_t j = 0; j < 4; ++j) {
+ const char c = next_char(str, &i);
+ if (!is_base64(c)) {
+ return result(EXESS_EXPECTED_BASE64, i);
+ }
+
+ in[j] = ustr[i++];
+ }
+
+ if (in[0] == '=' || in[1] == '=' || (in[2] == '=' && in[3] != '=')) {
+ return result(EXESS_BAD_VALUE, i);
+ }
+
+ const size_t n_bytes = 1u + (in[2] != '=') + (in[3] != '=');
+ if (o + n_bytes > out->size) {
+ return result(EXESS_NO_SPACE, i);
+ }
+
+ const uint8_t a1 = (uint8_t)(unmap(in[0]) << 2u);
+ const uint8_t a2 = unmap(in[1]) >> 4u;
+
+ uout[o++] = a1 | a2;
+
+ if (in[2] != '=') {
+ const uint8_t b1 = (uint8_t)(unmap(in[1]) << 4u) & 0xF0u;
+ const uint8_t b2 = unmap(in[2]) >> 2u;
+
+ uout[o++] = b1 | b2;
+ }
+
+ if (in[3] != '=') {
+ const uint8_t c1 = (uint8_t)(unmap(in[2]) << 6u) & 0xC0u;
+ const uint8_t c2 = unmap(in[3]);
+
+ uout[o++] = c1 | c2;
+ }
+ }
+
+ out->size = o;
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_write_base64(const size_t data_size,
+ const void* const data,
+ const size_t buf_size,
+ char* const buf)
+{
+ const size_t length = (data_size + 2) / 3 * 4;
+ if (!buf) {
+ return result(EXESS_SUCCESS, length);
+ }
+
+ if (buf_size < length + 1) {
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ uint8_t* const out = (uint8_t*)buf;
+
+ size_t o = 0;
+ for (size_t i = 0; i < data_size; i += 3, o += 4) {
+ uint8_t in[4] = {0, 0, 0, 0};
+ const size_t n_in = MIN(3, data_size - i);
+ memcpy(in, (const uint8_t*)data + i, n_in);
+
+ out[o] = map(in[0] >> 2u);
+ out[o + 1] = map(((in[0] & 0x03u) << 4u) | ((in[1] & 0xF0u) >> 4u));
+ out[o + 2] =
+ ((n_in > 1u) ? map(((in[1] & 0x0Fu) << 2u) | ((in[2] & 0xC0u) >> 6u))
+ : '=');
+
+ out[o + 3] = ((n_in > 2u) ? map(in[2] & 0x3Fu) : '=');
+ }
+
+ return end_write(EXESS_SUCCESS, buf_size, buf, o);
+}
diff --git a/subprojects/exess/src/bigint.c b/subprojects/exess/src/bigint.c
new file mode 100644
index 00000000..8c8a95c8
--- /dev/null
+++ b/subprojects/exess/src/bigint.c
@@ -0,0 +1,605 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "bigint.h"
+#include "macros.h"
+
+#include "int_math.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef uint64_t Hugit;
+
+static const uint32_t bigit_mask = ~(uint32_t)0;
+static const uint64_t carry_mask = (uint64_t) ~(uint32_t)0 << 32u;
+
+typedef struct {
+ unsigned bigits;
+ unsigned bits;
+} Offset;
+
+static inline Offset
+make_offset(const unsigned i)
+{
+ const unsigned bigits = i / BIGINT_BIGIT_BITS;
+ const unsigned bits = i - bigits * BIGINT_BIGIT_BITS;
+
+ const Offset offset = {bigits, bits};
+ return offset;
+}
+
+#ifndef NDEBUG
+static inline bool
+exess_bigint_is_clamped(const ExessBigint* num)
+{
+ return num->n_bigits == 0 || num->bigits[num->n_bigits - 1];
+}
+#endif
+
+void
+exess_bigint_shift_left(ExessBigint* num, const unsigned amount)
+{
+ assert(exess_bigint_is_clamped(num));
+ if (amount == 0 || num->n_bigits == 0) {
+ return;
+ }
+
+ const Offset offset = make_offset(amount);
+
+ assert(num->n_bigits + offset.bigits < BIGINT_MAX_BIGITS);
+ num->n_bigits += offset.bigits + (bool)offset.bits;
+
+ if (offset.bits == 0) { // Simple bigit-aligned shift
+ for (unsigned i = num->n_bigits - 1; i >= offset.bigits; --i) {
+ num->bigits[i] = num->bigits[i - offset.bigits];
+ }
+ } else { // Bigit + sub-bigit bit offset shift
+ const unsigned right_shift = BIGINT_BIGIT_BITS - offset.bits;
+ for (unsigned i = num->n_bigits - offset.bigits - 1; i > 0; --i) {
+ num->bigits[i + offset.bigits] =
+ (num->bigits[i] << offset.bits) | (num->bigits[i - 1] >> right_shift);
+ }
+
+ num->bigits[offset.bigits] = num->bigits[0] << offset.bits;
+ }
+
+ // Zero LSBs
+ for (unsigned i = 0; i < offset.bigits; ++i) {
+ num->bigits[i] = 0;
+ }
+
+ exess_bigint_clamp(num);
+ assert(exess_bigint_is_clamped(num));
+}
+
+void
+exess_bigint_zero(ExessBigint* num)
+{
+ static const ExessBigint zero = {{0}, 0};
+
+ *num = zero;
+}
+
+void
+exess_bigint_set(ExessBigint* num, const ExessBigint* value)
+{
+ *num = *value;
+}
+
+void
+exess_bigint_set_u32(ExessBigint* num, const uint32_t value)
+{
+ exess_bigint_zero(num);
+
+ num->bigits[0] = value;
+ num->n_bigits = (bool)value;
+}
+
+void
+exess_bigint_clamp(ExessBigint* num)
+{
+ while (num->n_bigits > 0 && num->bigits[num->n_bigits - 1] == 0) {
+ --num->n_bigits;
+ }
+}
+
+void
+exess_bigint_set_u64(ExessBigint* num, const uint64_t value)
+{
+ exess_bigint_zero(num);
+
+ num->bigits[0] = (Bigit)(value & bigit_mask);
+ num->bigits[1] = (Bigit)(value >> BIGINT_BIGIT_BITS);
+ num->n_bigits = num->bigits[1] ? 2u : num->bigits[0] ? 1u : 0u;
+}
+
+void
+exess_bigint_set_pow10(ExessBigint* num, const unsigned exponent)
+{
+ exess_bigint_set_u32(num, 1);
+ exess_bigint_multiply_pow10(num, exponent);
+}
+
+static uint32_t
+read_u32(const char* const str, uint32_t* result, uint32_t* n_digits)
+{
+ static const size_t uint32_digits10 = 9;
+
+ *result = *n_digits = 0;
+
+ uint32_t i = 0;
+ for (; str[i] && *n_digits < uint32_digits10; ++i) {
+ if (str[i] >= '0' && str[i] <= '9') {
+ *result = *result * 10u + (unsigned)(str[i] - '0');
+ *n_digits += 1;
+ } else if (str[i] != '.') {
+ break;
+ }
+ }
+
+ return i;
+}
+
+void
+exess_bigint_set_decimal_string(ExessBigint* num, const char* const str)
+{
+ exess_bigint_zero(num);
+
+ uint32_t pos = 0;
+ uint32_t n_digits = 0;
+ uint32_t n_read = 0;
+ uint32_t word = 0;
+ while ((n_read = read_u32(str + pos, &word, &n_digits))) {
+ exess_bigint_multiply_u32(num, (uint32_t)POW10[n_digits]);
+ exess_bigint_add_u32(num, word);
+ pos += n_read;
+ }
+
+ exess_bigint_clamp(num);
+}
+
+void
+exess_bigint_set_hex_string(ExessBigint* num, const char* const str)
+{
+ exess_bigint_zero(num);
+
+ // Read digits from right to left until we run off the beginning
+ const int length = (int)strlen(str);
+ char digit_buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ int i = length - 8;
+ for (; i >= 0; i -= 8) {
+ memcpy(digit_buf, str + i, 8);
+ num->bigits[num->n_bigits++] = (Bigit)strtoll(digit_buf, NULL, 16);
+ }
+
+ // Read leftovers into MSB if necessary
+ if (i > -8) {
+ memset(digit_buf, 0, sizeof(digit_buf));
+ memcpy(digit_buf, str, 8u + (unsigned)i);
+ num->bigits[num->n_bigits++] = (Bigit)strtoll(digit_buf, NULL, 16);
+ }
+
+ exess_bigint_clamp(num);
+}
+
+void
+exess_bigint_multiply_u32(ExessBigint* num, const uint32_t factor)
+{
+ switch (factor) {
+ case 0:
+ exess_bigint_zero(num);
+ return;
+ case 1:
+ return;
+ default:
+ break;
+ }
+
+ Hugit carry = 0;
+ for (unsigned i = 0; i < num->n_bigits; ++i) {
+ const Hugit p = (Hugit)factor * num->bigits[i];
+ const Hugit hugit = p + (carry & bigit_mask);
+
+ num->bigits[i] = (Bigit)(hugit & bigit_mask);
+
+ carry = (hugit >> 32u) + (carry >> 32u);
+ }
+
+ for (; carry; carry >>= 32u) {
+ assert(num->n_bigits + 1 <= BIGINT_MAX_BIGITS);
+ num->bigits[num->n_bigits++] = (Bigit)carry;
+ }
+}
+
+void
+exess_bigint_multiply_u64(ExessBigint* num, const uint64_t factor)
+{
+ switch (factor) {
+ case 0:
+ exess_bigint_zero(num);
+ return;
+ case 1:
+ return;
+ default:
+ break;
+ }
+
+ const Hugit f_lo = factor & bigit_mask;
+ const Hugit f_hi = factor >> 32u;
+
+ Hugit carry = 0;
+ for (unsigned i = 0; i < num->n_bigits; ++i) {
+ const Hugit p_lo = f_lo * num->bigits[i];
+ const Hugit p_hi = f_hi * num->bigits[i];
+ const Hugit hugit = p_lo + (carry & bigit_mask);
+
+ num->bigits[i] = (Bigit)(hugit & bigit_mask);
+ carry = p_hi + (hugit >> 32u) + (carry >> 32u);
+ }
+
+ for (; carry; carry >>= 32u) {
+ assert(num->n_bigits + 1 <= BIGINT_MAX_BIGITS);
+ num->bigits[num->n_bigits++] = (Bigit)(carry & bigit_mask);
+ }
+}
+
+void
+exess_bigint_multiply_pow10(ExessBigint* num, const unsigned exponent)
+{
+ /* To reduce multiplication, we exploit 10^e = (2*5)^e = 2^e * 5^e to
+ factor out an exponentiation by 5 instead of 10. So, we first multiply
+ by 5^e (hard), then by 2^e (just a single left shift). */
+
+ // 5^27, the largest power of 5 that fits in 64 bits
+ static const uint64_t pow5_27 = 7450580596923828125ull;
+
+ // Powers of 5 up to 5^13, the largest that fits in 32 bits
+ static const uint32_t pow5[] = {
+ 1,
+ 5,
+ 5 * 5,
+ 5 * 5 * 5,
+ 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ };
+
+ if (exponent == 0 || num->n_bigits == 0) {
+ return;
+ }
+
+ // Multiply by 5^27 until e < 27 so we can switch to 32 bits
+ unsigned e = exponent;
+ while (e >= 27) {
+ exess_bigint_multiply_u64(num, pow5_27);
+ e -= 27;
+ }
+
+ // Multiply by 5^13 until e < 13 so we have only one multiplication left
+ while (e >= 13) {
+ exess_bigint_multiply_u32(num, pow5[13]);
+ e -= 13;
+ }
+
+ // Multiply by the final 5^e (which may be zero, making this a noop)
+ exess_bigint_multiply_u32(num, pow5[e]);
+
+ // Finally multiply by 2^e
+ exess_bigint_shift_left(num, exponent);
+}
+
+int
+exess_bigint_compare(const ExessBigint* lhs, const ExessBigint* rhs)
+{
+ if (lhs->n_bigits < rhs->n_bigits) {
+ return -1;
+ }
+
+ if (lhs->n_bigits > rhs->n_bigits) {
+ return 1;
+ }
+
+ for (int i = (int)lhs->n_bigits - 1; i >= 0; --i) {
+ const Bigit bigit_l = lhs->bigits[i];
+ const Bigit bigit_r = rhs->bigits[i];
+ if (bigit_l < bigit_r) {
+ return -1;
+ }
+
+ if (bigit_l > bigit_r) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int
+exess_bigint_plus_compare(const ExessBigint* l,
+ const ExessBigint* p,
+ const ExessBigint* c)
+{
+ assert(exess_bigint_is_clamped(l));
+ assert(exess_bigint_is_clamped(p));
+ assert(exess_bigint_is_clamped(c));
+
+ if (l->n_bigits < p->n_bigits) {
+ return exess_bigint_plus_compare(p, l, c);
+ }
+
+ if (l->n_bigits + 1 < c->n_bigits) {
+ return -1;
+ }
+
+ if (l->n_bigits > c->n_bigits) {
+ return 1;
+ }
+
+ if (p->n_bigits < l->n_bigits && l->n_bigits < c->n_bigits) {
+ return -1;
+ }
+
+ Hugit borrow = 0;
+ for (int i = (int)c->n_bigits - 1; i >= 0; --i) {
+ const Bigit ai = l->bigits[i];
+ const Bigit bi = p->bigits[i];
+ const Bigit ci = c->bigits[i];
+ const Hugit sum = (Hugit)ai + bi;
+
+ if (sum > ci + borrow) {
+ return 1;
+ }
+
+ if ((borrow += ci - sum) > 1) {
+ return -1;
+ }
+
+ borrow <<= 32u;
+ }
+
+ return borrow ? -1 : 0;
+}
+
+void
+exess_bigint_add_u32(ExessBigint* lhs, const uint32_t rhs)
+{
+ if (lhs->n_bigits == 0) {
+ exess_bigint_set_u32(lhs, rhs);
+ return;
+ }
+
+ Hugit sum = (Hugit)lhs->bigits[0] + rhs;
+ Bigit carry = (Bigit)(sum >> 32u);
+
+ lhs->bigits[0] = (Bigit)(sum & bigit_mask);
+
+ unsigned i = 1;
+ for (; carry; ++i) {
+ assert(carry == 0 || carry == 1);
+
+ sum = (Hugit)carry + lhs->bigits[i];
+ lhs->bigits[i] = (Bigit)(sum & bigit_mask);
+ carry = (Bigit)((sum & carry_mask) >> 32u);
+ }
+
+ lhs->n_bigits = MAX(i, lhs->n_bigits);
+ assert(exess_bigint_is_clamped(lhs));
+}
+
+void
+exess_bigint_add(ExessBigint* lhs, const ExessBigint* rhs)
+{
+ assert(MAX(lhs->n_bigits, rhs->n_bigits) + 1 <= BIGINT_MAX_BIGITS);
+
+ bool carry = 0;
+ unsigned i = 0;
+ for (; i < rhs->n_bigits; ++i) {
+ const Hugit sum = (Hugit)lhs->bigits[i] + rhs->bigits[i] + carry;
+
+ lhs->bigits[i] = (Bigit)(sum & bigit_mask);
+ carry = (sum & carry_mask) >> 32u;
+ }
+
+ for (; carry; ++i) {
+ const Hugit sum = (Hugit)lhs->bigits[i] + carry;
+
+ lhs->bigits[i] = (Bigit)(sum & bigit_mask);
+ carry = (sum & carry_mask) >> 32u;
+ }
+
+ lhs->n_bigits = MAX(i, lhs->n_bigits);
+ assert(exess_bigint_is_clamped(lhs));
+}
+
+void
+exess_bigint_subtract(ExessBigint* lhs, const ExessBigint* rhs)
+{
+ assert(exess_bigint_is_clamped(lhs));
+ assert(exess_bigint_is_clamped(rhs));
+ assert(exess_bigint_compare(lhs, rhs) >= 0);
+
+ bool borrow = 0;
+ unsigned i = 0;
+ for (i = 0; i < rhs->n_bigits; ++i) {
+ const Bigit l = lhs->bigits[i];
+ const Bigit r = rhs->bigits[i];
+
+ lhs->bigits[i] = l - r - borrow;
+ borrow = l < r || (l == r && borrow);
+ }
+
+ for (; borrow; ++i) {
+ const Bigit l = lhs->bigits[i];
+
+ lhs->bigits[i] -= borrow;
+
+ borrow = l == 0;
+ }
+
+ exess_bigint_clamp(lhs);
+}
+
+static unsigned
+exess_bigint_leading_zeros(const ExessBigint* num)
+{
+ return 32 * (BIGINT_MAX_BIGITS - num->n_bigits) +
+ exess_clz32(num->bigits[num->n_bigits - 1]);
+}
+
+// EXESS_I_PURE_FUNC
+static Bigit
+exess_bigint_left_shifted_bigit_i(const ExessBigint* num,
+ const Offset amount,
+ const unsigned index)
+{
+ /* assert(exess_bigint_is_clamped(num)); */
+ if (amount.bigits == 0 && amount.bits == 0) {
+ return num->bigits[index];
+ }
+
+ if (index < amount.bigits) {
+ return 0;
+ }
+
+ if (amount.bits == 0) { // Simple bigit-aligned shift
+ return num->bigits[index - amount.bigits];
+ }
+
+ if (index == amount.bigits) { // Last non-zero bigit
+ return num->bigits[0] << amount.bits;
+ }
+
+ // Bigit + sub-bigit bit offset shift
+ const unsigned right_shift = BIGINT_BIGIT_BITS - amount.bits;
+ return (num->bigits[index - amount.bigits] << amount.bits) |
+ (num->bigits[index - amount.bigits - 1] >> right_shift);
+}
+
+Bigit
+exess_bigint_left_shifted_bigit(const ExessBigint* num,
+ const unsigned amount,
+ const unsigned index)
+{
+ return exess_bigint_left_shifted_bigit_i(num, make_offset(amount), index);
+}
+
+void
+exess_bigint_subtract_left_shifted(ExessBigint* lhs,
+ const ExessBigint* rhs,
+ const unsigned amount)
+{
+ assert(exess_bigint_is_clamped(lhs));
+ assert(exess_bigint_is_clamped(rhs));
+#ifndef NDEBUG
+ {
+ ExessBigint check_rhs = *rhs;
+ exess_bigint_shift_left(&check_rhs, amount);
+ assert(exess_bigint_compare(lhs, &check_rhs) >= 0);
+ }
+#endif
+
+ const Offset offset = make_offset(amount);
+ const unsigned r_n_bigits = rhs->n_bigits + offset.bigits + (bool)offset.bits;
+
+ bool borrow = 0;
+ unsigned i = 0;
+ for (i = 0; i < r_n_bigits; ++i) {
+ const Bigit l = lhs->bigits[i];
+ const Bigit r = exess_bigint_left_shifted_bigit_i(rhs, offset, i);
+
+ lhs->bigits[i] = l - r - borrow;
+ borrow = l < r || ((l == r) && borrow);
+ }
+
+ for (; borrow; ++i) {
+ const Bigit l = lhs->bigits[i];
+
+ lhs->bigits[i] -= borrow;
+
+ borrow = l == 0;
+ }
+
+ exess_bigint_clamp(lhs);
+}
+
+uint32_t
+exess_bigint_divmod(ExessBigint* lhs, const ExessBigint* rhs)
+{
+ assert(exess_bigint_is_clamped(lhs));
+ assert(exess_bigint_is_clamped(rhs));
+ assert(rhs->n_bigits > 0);
+ if (lhs->n_bigits < rhs->n_bigits) {
+ return 0;
+ }
+
+ uint32_t result = 0;
+ const Bigit r0 = rhs->bigits[rhs->n_bigits - 1];
+ const unsigned rlz = exess_bigint_leading_zeros(rhs);
+
+ // Shift and subtract until the LHS does not have more bigits
+ int big_steps = 0;
+ while (lhs->n_bigits > rhs->n_bigits) {
+ const unsigned llz = exess_bigint_leading_zeros(lhs);
+ const unsigned shift = rlz - llz - 1;
+
+ result += 1u << shift;
+ exess_bigint_subtract_left_shifted(lhs, rhs, shift);
+ ++big_steps;
+ }
+
+ // Handle simple termination cases
+ int cmp = exess_bigint_compare(lhs, rhs);
+ if (cmp < 0) {
+ return result;
+ }
+
+ if (cmp > 0 && lhs->n_bigits == 1) {
+ assert(rhs->n_bigits == 1);
+ const Bigit l0 = lhs->bigits[lhs->n_bigits - 1];
+
+ lhs->bigits[lhs->n_bigits - 1] = l0 % r0;
+ lhs->n_bigits -= (lhs->bigits[lhs->n_bigits - 1] == 0);
+ return result + l0 / r0;
+ }
+
+ // Both now have the same number of digits, finish with subtraction
+ int final_steps = 0;
+ for (; cmp >= 0; cmp = exess_bigint_compare(lhs, rhs)) {
+ const unsigned llz = exess_bigint_leading_zeros(lhs);
+ if (rlz == llz) {
+ // Both have the same number of leading zeros, just subtract
+ exess_bigint_subtract(lhs, rhs);
+ return result + 1;
+ }
+
+ const unsigned shift = rlz - llz - 1;
+ result += 1u << shift;
+ exess_bigint_subtract_left_shifted(lhs, rhs, shift);
+ ++final_steps;
+ }
+
+ return result;
+}
diff --git a/subprojects/exess/src/bigint.h b/subprojects/exess/src/bigint.h
new file mode 100644
index 00000000..088052f6
--- /dev/null
+++ b/subprojects/exess/src/bigint.h
@@ -0,0 +1,118 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_BIGINT_H
+#define EXESS_BIGINT_H
+
+#include "attributes.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+typedef uint32_t Bigit;
+
+/* We need enough precision for any double, the "largest" of which (using
+ absolute exponents) is the smallest subnormal ~= 5e-324. This is 1076 bits
+ long, but we need a bit more space for arithmetic. This is absurd, but such
+ is decimal. These are only used on the stack so it doesn't hurt too much.
+*/
+
+#define BIGINT_MAX_SIGNIFICANT_BITS 1280u
+#define BIGINT_BIGIT_BITS 32u
+#define BIGINT_MAX_BIGITS (BIGINT_MAX_SIGNIFICANT_BITS / BIGINT_BIGIT_BITS)
+
+typedef struct {
+ Bigit bigits[BIGINT_MAX_BIGITS];
+ unsigned n_bigits;
+} ExessBigint;
+
+void
+exess_bigint_zero(ExessBigint* num);
+
+size_t
+exess_bigint_print_hex(FILE* stream, const ExessBigint* num);
+
+void
+exess_bigint_clamp(ExessBigint* num);
+
+void
+exess_bigint_shift_left(ExessBigint* num, unsigned amount);
+
+void
+exess_bigint_set(ExessBigint* num, const ExessBigint* value);
+
+void
+exess_bigint_set_u32(ExessBigint* num, uint32_t value);
+
+void
+exess_bigint_set_u64(ExessBigint* num, uint64_t value);
+
+void
+exess_bigint_set_pow10(ExessBigint* num, unsigned exponent);
+
+void
+exess_bigint_set_decimal_string(ExessBigint* num, const char* str);
+
+void
+exess_bigint_set_hex_string(ExessBigint* num, const char* str);
+
+void
+exess_bigint_multiply_u32(ExessBigint* num, uint32_t factor);
+
+void
+exess_bigint_multiply_u64(ExessBigint* num, uint64_t factor);
+
+void
+exess_bigint_multiply_pow10(ExessBigint* num, unsigned exponent);
+
+EXESS_I_PURE_FUNC
+int
+exess_bigint_compare(const ExessBigint* lhs, const ExessBigint* rhs);
+
+void
+exess_bigint_add_u32(ExessBigint* lhs, uint32_t rhs);
+
+void
+exess_bigint_add(ExessBigint* lhs, const ExessBigint* rhs);
+
+void
+exess_bigint_subtract(ExessBigint* lhs, const ExessBigint* rhs);
+
+EXESS_I_PURE_FUNC
+Bigit
+exess_bigint_left_shifted_bigit(const ExessBigint* num,
+ unsigned amount,
+ unsigned index);
+
+/// Faster implementation of exess_bigint_subtract(lhs, rhs << amount)
+void
+exess_bigint_subtract_left_shifted(ExessBigint* lhs,
+ const ExessBigint* rhs,
+ unsigned amount);
+
+/// Faster implementation of exess_bigint_compare(l + p, c)
+EXESS_I_PURE_FUNC
+int
+exess_bigint_plus_compare(const ExessBigint* l,
+ const ExessBigint* p,
+ const ExessBigint* c);
+
+/// Divide and set `lhs` to modulo
+uint32_t
+exess_bigint_divmod(ExessBigint* lhs, const ExessBigint* rhs);
+
+#endif // EXESS_BIGINT_H
diff --git a/subprojects/exess/src/boolean.c b/subprojects/exess/src/boolean.c
new file mode 100644
index 00000000..32657da9
--- /dev/null
+++ b/subprojects/exess/src/boolean.c
@@ -0,0 +1,69 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+ExessResult
+exess_read_boolean(bool* const out, const char* const str)
+{
+ size_t i = skip_whitespace(str);
+ ExessResult r = {EXESS_EXPECTED_BOOLEAN, i};
+
+ *out = false;
+
+ switch (str[i]) {
+ case '0':
+ return end_read(EXESS_SUCCESS, str, ++i);
+
+ case '1':
+ *out = true;
+ return end_read(EXESS_SUCCESS, str, ++i);
+
+ case 't':
+ if (!strncmp(str + i, "true", 4)) {
+ *out = true;
+ return end_read(EXESS_SUCCESS, str, i + 4u);
+ }
+ break;
+
+ case 'f':
+ if (!strncmp(str + i, "false", 5)) {
+ return end_read(EXESS_SUCCESS, str, i + 5u);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return end_read(r.status, str, r.count);
+}
+
+ExessResult
+exess_write_boolean(const bool value, const size_t buf_size, char* const buf)
+{
+ return end_write(EXESS_SUCCESS,
+ buf_size,
+ buf,
+ value ? write_string(4, "true", buf_size, buf, 0)
+ : write_string(5, "false", buf_size, buf, 0));
+}
diff --git a/subprojects/exess/src/byte.c b/subprojects/exess/src/byte.c
new file mode 100644
index 00000000..4754d82d
--- /dev/null
+++ b/subprojects/exess/src/byte.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_byte(int8_t* const out, const char* const str)
+{
+ int64_t long_out = 0;
+ const ExessResult r = exess_read_long(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out < INT8_MIN || long_out > INT8_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (int8_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_byte(const int8_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_long(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/canonical.c b/subprojects/exess/src/canonical.c
new file mode 100644
index 00000000..f70aaecc
--- /dev/null
+++ b/subprojects/exess/src/canonical.c
@@ -0,0 +1,309 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef enum {
+ EXESS_NEGATIVE,
+ EXESS_ZERO,
+ EXESS_POSITIVE,
+} ExessIntegerKind;
+
+/// Return true iff `c` is "+" or "-"
+static inline bool
+is_sign(const int c)
+{
+ return c == '+' || c == '-';
+}
+
+/// Return true iff `c` is "0"
+static inline bool
+is_zero(const int c)
+{
+ return c == '0';
+}
+
+/// Return true iff `c` is "."
+static inline bool
+is_point(const int c)
+{
+ return c == '.';
+}
+
+// Scan forwards as long as `pred` returns true for characters
+static inline size_t
+scan(bool (*pred)(const int), const char* const str, size_t i)
+{
+ while (pred(str[i])) {
+ ++i;
+ }
+
+ return i;
+}
+
+// Skip the next character if `pred` returns true for it
+static inline size_t
+skip(bool (*pred)(const int), const char* const str, const size_t i)
+{
+ return i + (pred(str[i]) ? 1 : 0);
+}
+
+static ExessResult
+write_decimal(const char* const str, const size_t buf_size, char* const buf)
+{
+ size_t i = 0;
+
+ const size_t sign = scan(is_space, str, i); // Sign
+ const size_t leading = skip(is_sign, str, sign); // First digit
+ if (str[leading] != '.' && !is_digit(str[leading])) {
+ return result(EXESS_EXPECTED_DIGIT, sign);
+ }
+
+ const size_t first = scan(is_zero, str, leading); // First non-zero
+ const size_t point = scan(is_digit, str, first); // Decimal point
+ size_t last = scan(is_digit, str, skip(is_point, str, point)); // Last digit
+ const size_t end = scan(is_space, str, last); // Last non-space
+
+ const ExessStatus st = is_end(str[end]) ? EXESS_SUCCESS : EXESS_EXPECTED_END;
+
+ // Ignore trailing zeros
+ if (str[point] == '.') {
+ while (str[last - 1] == '0') {
+ --last;
+ }
+ }
+
+ // Add leading sign only if the number is negative
+ size_t o = 0;
+ if (str[sign] == '-') {
+ o += write_char('-', buf_size, buf, o);
+ }
+
+ // Handle zero as a special case (no non-zero digits to copy)
+ if (first == last) {
+ o += write_string(3, "0.0", buf_size, buf, o);
+ return result(EXESS_SUCCESS, o);
+ }
+
+ // Add leading zero if needed to have at least one digit before the point
+ if (str[first] == '.') {
+ o += write_char('0', buf_size, buf, o);
+ }
+
+ // Add digits
+ o += write_string(last - first, str + first, buf_size, buf, o);
+
+ if (str[point] != '.') {
+ // Add missing decimal suffix
+ o += write_string(2, ".0", buf_size, buf, o);
+ } else if (point == last - 1) {
+ // Add missing trailing zero after point
+ o += write_char('0', buf_size, buf, o);
+ }
+
+ return result(st, o);
+}
+
+static ExessResult
+write_integer(const char* const str,
+ const size_t buf_size,
+ char* const buf,
+ ExessIntegerKind* const kind)
+{
+ const size_t sign = scan(is_space, str, 0); // Sign
+ const size_t leading = skip(is_sign, str, sign); // First digit
+ if (!is_digit(str[leading])) {
+ return result(EXESS_EXPECTED_DIGIT, sign);
+ }
+
+ const size_t first = scan(is_zero, str, leading); // First non-zero
+ const size_t last = scan(is_digit, str, first); // Last digit
+ const size_t end = scan(is_space, str, last); // Last non-space
+
+ const ExessStatus st = is_end(str[end]) ? EXESS_SUCCESS : EXESS_EXPECTED_END;
+
+ // Handle zero as a special case (no non-zero digits to copy)
+ size_t o = 0;
+ if (first == last) {
+ o += write_char('0', buf_size, buf, o);
+ *kind = EXESS_ZERO;
+ return result(EXESS_SUCCESS, o);
+ }
+
+ // Add leading sign only if the number is negative
+ if (str[sign] == '-') {
+ *kind = EXESS_NEGATIVE;
+ o += write_char('-', buf_size, buf, o);
+ } else {
+ *kind = EXESS_POSITIVE;
+ }
+
+ // Add digits
+ o += write_string(last - first, str + first, buf_size, buf, o);
+
+ return result(st, o);
+}
+
+static ExessResult
+write_hex(const char* const str, const size_t buf_size, char* const buf)
+{
+ size_t i = 0;
+ size_t o = 0;
+
+ for (; str[i]; ++i) {
+ if (is_hexdig(str[i])) {
+ o += write_char(str[i], buf_size, buf, o);
+ } else if (!is_space(str[i])) {
+ return result(EXESS_EXPECTED_HEX, o);
+ }
+ }
+
+ if (o == 0 || o % 2 != 0) {
+ return result(EXESS_EXPECTED_HEX, o);
+ }
+
+ return result(EXESS_SUCCESS, o);
+}
+
+static ExessResult
+write_base64(const char* const str, const size_t buf_size, char* const buf)
+{
+ size_t i = 0;
+ size_t o = 0;
+
+ for (; str[i]; ++i) {
+ if (is_base64(str[i])) {
+ o += write_char(str[i], buf_size, buf, o);
+ } else if (!is_space(str[i])) {
+ return result(EXESS_EXPECTED_BASE64, o);
+ }
+ }
+
+ if (o == 0 || o % 4 != 0) {
+ return result(EXESS_EXPECTED_BASE64, o);
+ }
+
+ return result(EXESS_SUCCESS, o);
+}
+
+static ExessResult
+write_bounded(const char* const str,
+ const ExessDatatype datatype,
+ const size_t buf_size,
+ char* const buf)
+{
+ ExessVariant variant = {EXESS_NOTHING, {EXESS_SUCCESS}};
+ const ExessResult r = exess_read_variant(&variant, datatype, str);
+
+ return r.status ? r : exess_write_variant(variant, buf_size, buf);
+}
+
+ExessResult
+exess_write_canonical(const char* const str,
+ const ExessDatatype datatype,
+ const size_t buf_size,
+ char* const buf)
+{
+ ExessIntegerKind kind = EXESS_ZERO;
+ ExessResult r = {EXESS_UNSUPPORTED, 0};
+
+ switch (datatype) {
+ case EXESS_NOTHING:
+ break;
+
+ case EXESS_BOOLEAN:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_DECIMAL:
+ r = write_decimal(str, buf_size, buf);
+ break;
+
+ case EXESS_DOUBLE:
+ case EXESS_FLOAT:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ break;
+
+ case EXESS_NON_POSITIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_POSITIVE) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_NEGATIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_ZERO || kind == EXESS_POSITIVE) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_LONG:
+ case EXESS_INT:
+ case EXESS_SHORT:
+ case EXESS_BYTE:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_NEGATIVE) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_ULONG:
+ case EXESS_UINT:
+ case EXESS_USHORT:
+ case EXESS_UBYTE:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_POSITIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_NEGATIVE || kind == EXESS_ZERO) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ case EXESS_TIME:
+ case EXESS_DATE:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_HEX:
+ r = write_hex(str, buf_size, buf);
+ break;
+
+ case EXESS_BASE64:
+ r = write_base64(str, buf_size, buf);
+ }
+
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/coerce.c b/subprojects/exess/src/coerce.c
new file mode 100644
index 00000000..1a0c35dc
--- /dev/null
+++ b/subprojects/exess/src/coerce.c
@@ -0,0 +1,422 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <stdint.h>
+
+/* Limits for the range of integers that can be exactly represented in floating
+ point types. Note that these limits are one less than the largest value,
+ since values larger than that may round to it which causes problems with
+ perfect round-tripping. For example, 16777217 when parsed as a float will
+ result in 1.6777216E7, which a "lossless" coercion would then convert to
+ 16777216. */
+
+#define MAX_FLOAT_INT 16777215
+#define MAX_DOUBLE_INT 9007199254740991L
+
+static ExessVariant
+coerce_long_in_range(const ExessVariant variant,
+ const int64_t min,
+ const int64_t max)
+{
+ const ExessVariant result = exess_coerce(variant, EXESS_LONG, EXESS_LOSSLESS);
+ if (result.datatype == EXESS_LONG) {
+ if (result.value.as_long < min || result.value.as_long > max) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ }
+
+ return result;
+}
+
+static ExessVariant
+coerce_to_long(const ExessVariant variant, const ExessCoercionFlags coercions)
+{
+ switch (variant.datatype) {
+ case EXESS_NOTHING:
+ return variant;
+
+ case EXESS_BOOLEAN:
+ return exess_make_long(variant.value.as_bool);
+
+ case EXESS_DECIMAL:
+ case EXESS_DOUBLE:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ variant.value.as_double > trunc(variant.value.as_double)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (variant.value.as_double < (double)-MAX_DOUBLE_INT ||
+ variant.value.as_double > (double)MAX_DOUBLE_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long(lrint(variant.value.as_double));
+
+ case EXESS_FLOAT:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ variant.value.as_float > truncf(variant.value.as_float)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (variant.value.as_float < (float)-MAX_FLOAT_INT ||
+ variant.value.as_float > (float)MAX_FLOAT_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long(lrintf(variant.value.as_float));
+
+ case EXESS_INTEGER:
+ case EXESS_NON_POSITIVE_INTEGER:
+ case EXESS_NEGATIVE_INTEGER:
+ case EXESS_LONG:
+ return exess_make_long(variant.value.as_long);
+
+ case EXESS_INT:
+ return exess_make_long(variant.value.as_int);
+
+ case EXESS_SHORT:
+ return exess_make_long(variant.value.as_short);
+
+ case EXESS_BYTE:
+ return exess_make_long(variant.value.as_byte);
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ return exess_make_long(variant.value.as_long);
+
+ case EXESS_ULONG:
+ if (variant.value.as_ulong > INT64_MAX) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long((int64_t)variant.value.as_ulong);
+
+ case EXESS_UINT:
+ return exess_make_long(variant.value.as_uint);
+
+ case EXESS_USHORT:
+ return exess_make_long(variant.value.as_ushort);
+
+ case EXESS_UBYTE:
+ return exess_make_long(variant.value.as_ubyte);
+
+ case EXESS_POSITIVE_INTEGER:
+ if (variant.value.as_ulong > INT64_MAX) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long((int64_t)variant.value.as_ulong);
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ case EXESS_TIME:
+ case EXESS_DATE:
+ case EXESS_HEX:
+ case EXESS_BASE64:
+ break;
+ }
+
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+}
+
+static ExessVariant
+coerce_ulong_in_range(const ExessVariant variant, const uint64_t max)
+{
+ const ExessVariant result =
+ exess_coerce(variant, EXESS_ULONG, EXESS_LOSSLESS);
+
+ if (result.datatype == EXESS_ULONG) {
+ if (variant.value.as_ulong > max) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ }
+
+ return result;
+}
+
+static ExessVariant
+coerce_to_ulong(const ExessVariant value, const ExessCoercionFlags coercions)
+{
+ switch (value.datatype) {
+ case EXESS_NOTHING:
+ return value;
+
+ case EXESS_BOOLEAN:
+ return exess_make_ulong(value.value.as_bool);
+
+ case EXESS_DECIMAL:
+ case EXESS_DOUBLE:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ value.value.as_double > trunc(value.value.as_double)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (value.value.as_double < 0.0 ||
+ value.value.as_double > (double)MAX_DOUBLE_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)lrint(value.value.as_double));
+
+ case EXESS_FLOAT:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ value.value.as_float > truncf(value.value.as_float)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (value.value.as_float < 0.0f ||
+ value.value.as_float > (float)MAX_FLOAT_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)lrintf(value.value.as_float));
+
+ case EXESS_INTEGER:
+ case EXESS_NON_POSITIVE_INTEGER:
+ case EXESS_NEGATIVE_INTEGER:
+ case EXESS_LONG:
+ if (value.value.as_long < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_long);
+
+ case EXESS_INT:
+ if (value.value.as_int < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_int);
+
+ case EXESS_SHORT:
+ if (value.value.as_short < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_short);
+
+ case EXESS_BYTE:
+ if (value.value.as_byte < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_byte);
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ return exess_make_ulong(value.value.as_ulong);
+
+ case EXESS_UINT:
+ return exess_make_ulong(value.value.as_uint);
+
+ case EXESS_USHORT:
+ return exess_make_ulong(value.value.as_ushort);
+
+ case EXESS_UBYTE:
+ return exess_make_ulong(value.value.as_ubyte);
+
+ case EXESS_POSITIVE_INTEGER:
+ return exess_make_ulong(value.value.as_ulong);
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ case EXESS_TIME:
+ case EXESS_DATE:
+ case EXESS_HEX:
+ case EXESS_BASE64:
+ break;
+ }
+
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+}
+
+ExessVariant
+exess_coerce(const ExessVariant value,
+ const ExessDatatype datatype,
+ const ExessCoercionFlags coercions)
+{
+ if (datatype == value.datatype) {
+ return value;
+ }
+
+ ExessVariant result = value;
+
+ switch (datatype) {
+ case EXESS_NOTHING:
+ break;
+
+ case EXESS_BOOLEAN:
+ result = exess_coerce(value, EXESS_LONG, coercions);
+ if (result.datatype == EXESS_LONG) {
+ if (!(coercions & (ExessCoercionFlags)EXESS_TRUNCATE) &&
+ result.value.as_long != 0 && result.value.as_long != 1) {
+ return exess_make_nothing(EXESS_WOULD_TRUNCATE);
+ }
+
+ return exess_make_boolean(result.value.as_long != 0);
+ }
+ break;
+
+ case EXESS_DECIMAL:
+ // FIXME
+
+ case EXESS_DOUBLE:
+ if (value.datatype == EXESS_DECIMAL) {
+ return exess_make_double(value.value.as_double);
+ }
+
+ if (value.datatype == EXESS_FLOAT) {
+ return exess_make_double((double)value.value.as_float);
+ }
+
+ result = coerce_long_in_range(value, -MAX_DOUBLE_INT, MAX_DOUBLE_INT);
+ if (result.datatype == EXESS_LONG) {
+ return exess_make_double((double)result.value.as_long);
+ }
+
+ break;
+
+ case EXESS_FLOAT:
+ if (value.datatype == EXESS_DECIMAL || value.datatype == EXESS_DOUBLE) {
+ if (!(coercions & (ExessCoercionFlags)EXESS_REDUCE_PRECISION)) {
+ return exess_make_nothing(EXESS_WOULD_REDUCE_PRECISION);
+ }
+
+ return exess_make_float((float)result.value.as_double);
+ } else {
+ result = coerce_long_in_range(value, -MAX_FLOAT_INT, MAX_FLOAT_INT);
+ if (result.datatype == EXESS_LONG) {
+ return exess_make_float((float)result.value.as_long);
+ }
+ }
+
+ break;
+
+ case EXESS_INTEGER:
+ result = coerce_to_long(value, coercions);
+ break;
+
+ case EXESS_NON_POSITIVE_INTEGER:
+ result = coerce_to_long(value, coercions);
+ if (result.datatype == EXESS_LONG && result.value.as_long > 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ break;
+
+ case EXESS_NEGATIVE_INTEGER:
+ result = coerce_to_long(value, coercions);
+ if (result.datatype == EXESS_LONG && result.value.as_long >= 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ break;
+
+ case EXESS_LONG:
+ return coerce_to_long(value, coercions);
+
+ case EXESS_INT:
+ result = coerce_long_in_range(value, INT32_MIN, INT32_MAX);
+ break;
+
+ case EXESS_SHORT:
+ result = coerce_long_in_range(value, INT16_MIN, INT16_MAX);
+ break;
+
+ case EXESS_BYTE:
+ result = coerce_long_in_range(value, INT8_MIN, INT8_MAX);
+ break;
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ result = coerce_to_ulong(value, coercions);
+ break;
+
+ case EXESS_UINT:
+ result = coerce_ulong_in_range(value, UINT32_MAX);
+ break;
+
+ case EXESS_USHORT:
+ result = coerce_ulong_in_range(value, UINT16_MAX);
+ break;
+
+ case EXESS_UBYTE:
+ result = coerce_ulong_in_range(value, UINT8_MAX);
+ break;
+
+ case EXESS_POSITIVE_INTEGER:
+ result = coerce_to_ulong(value, coercions);
+ if (result.datatype == EXESS_ULONG && result.value.as_ulong == 0u) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ break;
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+
+ case EXESS_TIME:
+ if (value.datatype != EXESS_DATETIME) {
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+ }
+
+ if (coercions & (ExessCoercionFlags)EXESS_TRUNCATE) {
+ const ExessTime time = {
+ {value.value.as_datetime.is_utc ? 0 : EXESS_LOCAL},
+ value.value.as_datetime.hour,
+ value.value.as_datetime.minute,
+ value.value.as_datetime.second,
+ value.value.as_datetime.nanosecond};
+
+ return exess_make_time(time);
+ }
+
+ return exess_make_nothing(EXESS_WOULD_TRUNCATE);
+
+ case EXESS_DATE:
+ if (value.datatype != EXESS_DATETIME) {
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+ }
+
+ if (coercions & (ExessCoercionFlags)EXESS_TRUNCATE) {
+ const ExessDate date = {
+ value.value.as_datetime.year,
+ value.value.as_datetime.month,
+ value.value.as_datetime.day,
+ {value.value.as_datetime.is_utc ? 0 : EXESS_LOCAL}};
+ return exess_make_date(date);
+ }
+
+ return exess_make_nothing(EXESS_WOULD_TRUNCATE);
+
+ case EXESS_HEX:
+ return (value.datatype == EXESS_BASE64)
+ ? exess_make_hex(value.value.as_blob)
+ : exess_make_nothing(EXESS_UNSUPPORTED);
+
+ case EXESS_BASE64:
+ return (value.datatype == EXESS_HEX)
+ ? exess_make_base64(value.value.as_blob)
+ : exess_make_nothing(EXESS_UNSUPPORTED);
+ }
+
+ if (result.datatype != EXESS_NOTHING) {
+ result.datatype = datatype;
+ }
+
+ return result;
+}
diff --git a/subprojects/exess/src/datatype.c b/subprojects/exess/src/datatype.c
new file mode 100644
index 00000000..c7789597
--- /dev/null
+++ b/subprojects/exess/src/datatype.c
@@ -0,0 +1,79 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+#define N_DATATYPES 24
+
+static const char* EXESS_NONNULL const uris[N_DATATYPES + 1] = {
+ NULL, //
+ EXESS_XSD_URI "boolean", //
+ EXESS_XSD_URI "decimal", //
+ EXESS_XSD_URI "double", //
+ EXESS_XSD_URI "float", //
+ EXESS_XSD_URI "integer", //
+ EXESS_XSD_URI "nonPositiveInteger", //
+ EXESS_XSD_URI "negativeInteger", //
+ EXESS_XSD_URI "long", //
+ EXESS_XSD_URI "int", //
+ EXESS_XSD_URI "short", //
+ EXESS_XSD_URI "byte", //
+ EXESS_XSD_URI "nonNegativeInteger", //
+ EXESS_XSD_URI "unsignedLong", //
+ EXESS_XSD_URI "unsignedInt", //
+ EXESS_XSD_URI "unsignedShort", //
+ EXESS_XSD_URI "unsignedByte", //
+ EXESS_XSD_URI "positiveInteger", //
+ EXESS_XSD_URI "duration", //
+ EXESS_XSD_URI "datetime", //
+ EXESS_XSD_URI "time", //
+ EXESS_XSD_URI "date", //
+ EXESS_XSD_URI "hexBinary", //
+ EXESS_XSD_URI "base64Binary", //
+};
+
+const char*
+exess_datatype_uri(const ExessDatatype datatype)
+{
+ return (datatype > EXESS_NOTHING && datatype <= EXESS_BASE64) ? uris[datatype]
+ : NULL;
+}
+
+ExessDatatype
+exess_datatype_from_uri(const char* const uri)
+{
+ static const size_t xsd_len = sizeof(EXESS_XSD_URI) - 1;
+
+ if (!strncmp(uri, EXESS_XSD_URI, xsd_len)) {
+ const char* const name = uri + xsd_len;
+ for (size_t i = 1; i < N_DATATYPES; ++i) {
+ if (!strcmp(name, uris[i] + xsd_len)) {
+ return (ExessDatatype)i;
+ }
+ }
+ }
+
+ return EXESS_NOTHING;
+}
+
+bool
+exess_datatype_is_bounded(const ExessDatatype datatype)
+{
+ return (datatype < N_DATATYPES) ? exess_max_lengths[datatype] != 0 : false;
+}
diff --git a/subprojects/exess/src/date.c b/subprojects/exess/src/date.c
new file mode 100644
index 00000000..77b336f7
--- /dev/null
+++ b/subprojects/exess/src/date.c
@@ -0,0 +1,112 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "date_utils.h"
+#include "read_utils.h"
+#include "timezone.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+ExessResult
+read_date_numbers(ExessDate* const out, const char* const str)
+{
+ // Read year at the beginning
+ size_t i = skip_whitespace(str);
+ ExessResult r = read_year_number(&out->year, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read year-month delimiter
+ i += r.count;
+ if (str[i] != '-') {
+ return result(EXESS_EXPECTED_DASH, i);
+ }
+
+ // Read month
+ ++i;
+ if ((r = read_two_digit_number(&out->month, 1, 12, str + i)).status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read month-day delimiter
+ i += r.count;
+ if (str[i] != '-') {
+ return result(EXESS_EXPECTED_DASH, i);
+ }
+
+ // Read day
+ ++i;
+ if ((r = read_two_digit_number(&out->day, 1, 31, str + i)).status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Check that day is in range
+ i += r.count;
+ if (out->day > days_in_month(out->year, out->month)) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_read_date(ExessDate* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Read YYYY-MM-DD numbers
+ size_t i = skip_whitespace(str);
+ ExessResult r = read_date_numbers(out, str + i);
+
+ i += r.count;
+ if (r.status || is_end(str[i])) {
+ out->zone.quarter_hours = EXESS_LOCAL;
+ return result(r.status, i);
+ }
+
+ // Read timezone
+ r = exess_read_timezone(&out->zone, str + i);
+
+ return result(r.status, i + r.count);
+}
+
+ExessResult
+exess_write_date(const ExessDate value, const size_t buf_size, char* const buf)
+{
+ if (value.month < 1 || value.month > 12 || value.day < 1 || value.day > 31) {
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ ExessResult r = write_year_number(value.year, buf_size, buf);
+ size_t o = r.count;
+ if (r.status) {
+ return end_write(r.status, buf_size, buf, o);
+ }
+
+ o += write_char('-', buf_size, buf, o);
+ o += write_two_digit_number(value.month, buf_size, buf, o);
+ o += write_char('-', buf_size, buf, o);
+ o += write_two_digit_number(value.day, buf_size, buf, o);
+
+ r = write_timezone(value.zone, buf_size, buf, o);
+
+ return end_write(r.status, buf_size, buf, o + r.count);
+}
diff --git a/subprojects/exess/src/date_utils.h b/subprojects/exess/src/date_utils.h
new file mode 100644
index 00000000..b864925e
--- /dev/null
+++ b/subprojects/exess/src/date_utils.h
@@ -0,0 +1,65 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_DATE_UTILS_H
+#define EXESS_DATE_UTILS_H
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+static inline bool
+is_leap_year(const int64_t year)
+{
+ if (year % 4) {
+ return false;
+ }
+
+ if (year % 100) {
+ return true;
+ }
+
+ if (year % 400) {
+ return false;
+ }
+
+ return true;
+}
+
+static inline uint8_t
+days_in_month(const int16_t year, const uint8_t month)
+{
+ return month == 2u ? (is_leap_year(year) ? 29u : 28u)
+ : (uint8_t)(30u + (month + (month / 8u)) % 2u);
+}
+
+ExessResult
+read_year_number(int16_t* out, const char* str);
+
+ExessResult
+write_year_number(int16_t value, size_t buf_size, char* buf);
+
+/// Read YYYY-MM-DD date numbers without a timezone
+ExessResult
+read_date_numbers(ExessDate* out, const char* str);
+
+EXESS_CONST_FUNC
+size_t
+exess_timezone_string_length(ExessTimezone value);
+
+#endif // EXESS_DATE_UTILS_H
diff --git a/subprojects/exess/src/datetime.c b/subprojects/exess/src/datetime.c
new file mode 100644
index 00000000..0fe56afe
--- /dev/null
+++ b/subprojects/exess/src/datetime.c
@@ -0,0 +1,265 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "date_utils.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "time_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+static inline ExessDateTime
+infinite_future(const bool is_utc)
+{
+ const ExessDateTime result = {INT16_MAX,
+ UINT8_MAX,
+ UINT8_MAX,
+ is_utc,
+ UINT8_MAX,
+ UINT8_MAX,
+ UINT8_MAX,
+ UINT32_MAX};
+
+ return result;
+}
+
+static inline ExessDateTime
+infinite_past(const bool is_utc)
+{
+ const ExessDateTime result = {INT16_MIN, 0, 0, is_utc, 0, 0, 0, 0};
+
+ return result;
+}
+
+static int32_t
+modulo(const int32_t a, const int32_t low, const int32_t high)
+{
+ return ((a - low) % (high - low)) + low;
+}
+
+static int32_t
+quotient(const int32_t a, const int32_t low, const int32_t high)
+{
+ return (a - low) / (high - low);
+}
+
+ExessDateTime
+exess_add_datetime_duration(const ExessDateTime s, const ExessDuration d)
+{
+ /*
+ See <https://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes>.
+ This algorithm is modified to support subtraction when d is negative.
+ */
+
+ const int32_t d_year = d.months / 12;
+ const int32_t d_month = d.months % 12;
+ const int32_t d_day = d.seconds / (24 * 60 * 60);
+ const int32_t d_hour = d.seconds / 60 / 60 % 24;
+ const int32_t d_minute = d.seconds / 60 % 60;
+ const int32_t d_second = d.seconds % 60;
+ const int32_t d_nanosecond = d.nanoseconds;
+
+ ExessDateTime e = {0, 0u, 0u, s.is_utc, 0u, 0u, 0u, 0u};
+ int32_t temp = 0;
+ int32_t carry = 0;
+
+ // Months (may be modified additionally below)
+ temp = s.month + d_month;
+ if (temp <= 0) {
+ e.month = (uint8_t)(12 + modulo(temp, 1, 13));
+ carry = quotient(temp, 1, 13) - 1;
+ } else {
+ e.month = (uint8_t)modulo(temp, 1, 13);
+ carry = quotient(temp, 1, 13);
+ }
+
+ // Years (may be modified additionally below)
+ temp = s.year + d_year + carry;
+ if (temp > INT16_MAX) {
+ return infinite_future(s.is_utc);
+ }
+
+ if (temp < INT16_MIN) {
+ return infinite_past(s.is_utc);
+ }
+
+ e.year = (int16_t)temp;
+
+ // Nanoseconds
+ temp = (int32_t)s.nanosecond + d_nanosecond;
+ if (temp < 0) {
+ e.nanosecond = (uint32_t)(1000000000 + (temp % 1000000000));
+ carry = temp / 1000000000 - 1;
+ } else {
+ e.nanosecond = (uint32_t)(temp % 1000000000);
+ carry = temp / 1000000000;
+ }
+
+ // Seconds
+ temp = s.second + d_second + carry;
+ if (temp < 0) {
+ e.second = (uint8_t)(60 + (temp % 60));
+ carry = temp / 60 - 1;
+ } else {
+ e.second = (uint8_t)(temp % 60);
+ carry = temp / 60;
+ }
+
+ // Minutes
+ temp = s.minute + d_minute + carry;
+ if (temp < 0) {
+ e.minute = (uint8_t)(60 + (temp % 60));
+ carry = temp / 60 - 1;
+ } else {
+ e.minute = (uint8_t)(temp % 60);
+ carry = temp / 60;
+ }
+
+ // Hours
+ temp = s.hour + d_hour + carry;
+ if (temp < 0) {
+ e.hour = (uint8_t)(24 + (temp % 24));
+ carry = temp / 24 - 1;
+ } else {
+ e.hour = (uint8_t)(temp % 24);
+ carry = temp / 24;
+ }
+
+ /*
+ Carry days into months and years as necessary. Note that the algorithm in
+ the spec first clamps here, but we don't because no such datetime should
+ exist (exess_read_datetime refuses to read them)
+ */
+ int32_t day = s.day + d_day + carry;
+ while (day < 1 || day > days_in_month(e.year, e.month)) {
+ if (day < 1) {
+ if (e.month == 1) {
+ if (e.year == INT16_MIN) {
+ return infinite_past(s.is_utc);
+ }
+
+ --e.year;
+ e.month = 12;
+ day += days_in_month(e.year, e.month);
+ } else {
+ --e.month;
+ day += days_in_month(e.year, e.month);
+ }
+ } else {
+ day -= days_in_month(e.year, e.month);
+ if (++e.month > 12) {
+ if (e.year == INT16_MAX) {
+ return infinite_future(s.is_utc);
+ }
+
+ ++e.year;
+ e.month = (uint8_t)modulo(e.month, 1, 13);
+ }
+ }
+ }
+
+ e.day = (uint8_t)day;
+
+ return e;
+}
+
+ExessResult
+exess_read_datetime(ExessDateTime* const out, const char* const str)
+{
+ out->year = 0;
+ out->month = 0;
+ out->day = 0;
+
+ // Read date
+ ExessDate date = {0, 0u, 0u, {EXESS_LOCAL}};
+ const ExessResult dr = read_date_numbers(&date, str);
+ if (dr.status) {
+ return dr;
+ }
+
+ size_t i = dr.count;
+ if (str[i] != 'T') {
+ return result(EXESS_EXPECTED_TIME_SEP, i);
+ }
+
+ ++i;
+
+ // Read time
+ ExessTime time = {{INT8_MAX}, 0u, 0u, 0u, 0u};
+ const ExessResult tr = exess_read_time(&time, str + i);
+ if (tr.status) {
+ return result(tr.status, i + tr.count);
+ }
+
+ i += tr.count;
+
+ const ExessDateTime datetime = {date.year,
+ date.month,
+ date.day,
+ time.zone.quarter_hours != EXESS_LOCAL,
+ time.hour,
+ time.minute,
+ time.second,
+ time.nanosecond};
+
+ if (datetime.is_utc) {
+ const ExessDuration tz_duration = {
+ 0u, -time.zone.quarter_hours * 15 * 60, 0};
+
+ *out = exess_add_datetime_duration(datetime, tz_duration);
+ } else {
+ *out = datetime;
+ }
+
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_write_datetime(const ExessDateTime value,
+ const size_t buf_size,
+ char* const buf)
+{
+ const ExessTimezone local = {EXESS_LOCAL};
+ const ExessDate date = {value.year, value.month, value.day, local};
+ const ExessTimezone zone = {value.is_utc ? 0 : EXESS_LOCAL};
+ const ExessTime time = {
+ zone, value.hour, value.minute, value.second, value.nanosecond};
+
+ if (!in_range(value.month, 1, 12) || !in_range(value.day, 1, 31) ||
+ !in_range(value.hour, 0, 24) || !in_range(value.minute, 0, 59) ||
+ !in_range(value.second, 0, 59) || value.nanosecond > 999999999) {
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ // Write date
+ ExessResult dr = exess_write_date(date, buf_size, buf);
+ if (dr.status) {
+ return end_write(dr.status, buf_size, buf, dr.count);
+ }
+
+ // Write time delimiter
+ size_t o = dr.count + write_char('T', buf_size, buf, dr.count);
+
+ // Write time with timezone
+ const ExessResult tr = write_time(time, buf_size, buf, o);
+
+ return end_write(tr.status, buf_size, buf, o + tr.count);
+}
diff --git a/subprojects/exess/src/decimal.c b/subprojects/exess/src/decimal.c
new file mode 100644
index 00000000..a8ce8ad5
--- /dev/null
+++ b/subprojects/exess/src/decimal.c
@@ -0,0 +1,267 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "decimal.h"
+#include "digits.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "strtod.h"
+#include "warnings.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <assert.h>
+
+typedef enum {
+ EXESS_POINT_AFTER, ///< Decimal point is after all significant digits
+ EXESS_POINT_BEFORE, ///< Decimal point is before all significant digits
+ EXESS_POINT_BETWEEN, ///< Decimal point is between significant digits
+} ExessPointLocation;
+
+typedef struct {
+ ExessPointLocation point_loc; ///< Location of decimal point
+ unsigned n_zeros_before; ///< Number of extra zeros before point
+ unsigned n_zeros_after; ///< Number of extra zeros after point
+} DecimalMetrics;
+
+static DecimalMetrics
+decimal_metrics(const ExessDecimalDouble count)
+{
+ const int expt =
+ count.expt >= 0 ? (count.expt - (int)count.n_digits + 1) : count.expt;
+
+ DecimalMetrics metrics = {EXESS_POINT_AFTER, 0u, 0u};
+
+ if (count.expt >= (int)count.n_digits - 1) {
+ metrics.point_loc = EXESS_POINT_AFTER;
+ metrics.n_zeros_before = (unsigned)count.expt - (count.n_digits - 1u);
+ metrics.n_zeros_after = 1u;
+ } else if (count.expt < 0) {
+ metrics.point_loc = EXESS_POINT_BEFORE;
+ metrics.n_zeros_before = 1u;
+ metrics.n_zeros_after = (unsigned)(-expt - 1);
+ } else {
+ metrics.point_loc = EXESS_POINT_BETWEEN;
+ }
+
+ return metrics;
+}
+
+static ExessNumberKind
+number_kind(const double d)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ const int fpclass = fpclassify(d);
+ const bool is_negative = signbit(d);
+ EXESS_RESTORE_WARNINGS
+
+ switch (fpclass) {
+ case FP_ZERO:
+ return is_negative ? EXESS_NEGATIVE_ZERO : EXESS_POSITIVE_ZERO;
+ case FP_INFINITE:
+ return is_negative ? EXESS_NEGATIVE_INFINITY : EXESS_POSITIVE_INFINITY;
+ case FP_NORMAL:
+ case FP_SUBNORMAL:
+ return is_negative ? EXESS_NEGATIVE : EXESS_POSITIVE;
+ default:
+ break;
+ }
+
+ return EXESS_NAN;
+}
+
+ExessDecimalDouble
+exess_measure_decimal(const double d, const unsigned max_precision)
+{
+ ExessDecimalDouble value = {number_kind(d), 0, 0, {0}};
+
+ if (value.kind != EXESS_NEGATIVE && value.kind != EXESS_POSITIVE) {
+ return value;
+ }
+
+ // Get decimal digits
+ const double abs_d = fabs(d);
+ const ExessDigitCount count =
+ exess_digits(abs_d, value.digits, max_precision);
+
+ assert(count.count == 1 || value.digits[count.count - 1] != '0');
+
+ value.n_digits = count.count;
+ value.expt = count.expt;
+
+ return value;
+}
+
+ExessDecimalDouble
+exess_measure_float(const float f)
+{
+ return exess_measure_decimal((double)f, FLT_DECIMAL_DIG);
+}
+
+ExessDecimalDouble
+exess_measure_double(const double d)
+{
+ return exess_measure_decimal(d, DBL_DECIMAL_DIG);
+}
+
+static size_t
+exess_decimal_double_string_length(const ExessDecimalDouble decimal)
+{
+ switch (decimal.kind) {
+ case EXESS_NEGATIVE:
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return 0;
+ case EXESS_NEGATIVE_ZERO:
+ return 4;
+ case EXESS_POSITIVE_ZERO:
+ return 3;
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ case EXESS_NAN:
+ return 0;
+ }
+
+ const DecimalMetrics metrics = decimal_metrics(decimal);
+ const unsigned n_zeros = metrics.n_zeros_before + metrics.n_zeros_after;
+ const bool is_negative = decimal.kind == EXESS_NEGATIVE;
+
+ return is_negative + decimal.n_digits + 1 + n_zeros;
+}
+
+static size_t
+copy_digits(char* const dest, const char* const src, const size_t n)
+{
+ memcpy(dest, src, n);
+ return n;
+}
+
+static size_t
+set_zeros(char* const dest, const size_t n)
+{
+ memset(dest, '0', n);
+ return n;
+}
+
+static ExessResult
+read_decimal_number(double* const out, const char* const str)
+{
+ *out = (double)NAN;
+
+ if (str[0] == '+' || str[0] == '-') {
+ if (str[1] != '.' && !is_digit(str[1])) {
+ return result(EXESS_EXPECTED_DIGIT, 1);
+ }
+ } else if (str[0] != '.' && !is_digit(str[0])) {
+ return result(EXESS_EXPECTED_DIGIT, 0);
+ }
+
+ const size_t i = skip_whitespace(str);
+ ExessDecimalDouble in = {EXESS_NAN, 0u, 0, {0}};
+ const ExessResult r = parse_decimal(&in, str + i);
+
+ if (!r.status) {
+ *out = parsed_double_to_double(in);
+ }
+
+ return result(r.status, i + r.count);
+}
+
+ExessResult
+exess_read_decimal(double* const out, const char* const str)
+{
+ const size_t i = skip_whitespace(str);
+ const ExessResult r = read_decimal_number(out, str + i);
+
+ return end_read(r.status, str, i + r.count);
+}
+
+ExessResult
+exess_write_decimal_double(const ExessDecimalDouble decimal,
+ const size_t buf_size,
+ char* const buf)
+{
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_decimal_double_string_length(decimal));
+ }
+
+ size_t i = 0;
+ if (buf_size < 3) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ switch (decimal.kind) {
+ case EXESS_NEGATIVE:
+ buf[i++] = '-';
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ case EXESS_NEGATIVE_ZERO:
+ return write_special(4, "-0.0", buf_size, buf);
+ case EXESS_POSITIVE_ZERO:
+ return write_special(3, "0.0", buf_size, buf);
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ case EXESS_NAN:
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ const DecimalMetrics metrics = decimal_metrics(decimal);
+ const unsigned n_zeros = metrics.n_zeros_before + metrics.n_zeros_after;
+ if (buf_size - i <= decimal.n_digits + 1 + n_zeros) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ if (metrics.point_loc == EXESS_POINT_AFTER) {
+ i += copy_digits(buf + i, decimal.digits, decimal.n_digits);
+ i += set_zeros(buf + i, metrics.n_zeros_before);
+ buf[i++] = '.';
+ buf[i++] = '0';
+ } else if (metrics.point_loc == EXESS_POINT_BEFORE) {
+ buf[i++] = '0';
+ buf[i++] = '.';
+ i += set_zeros(buf + i, metrics.n_zeros_after);
+ i += copy_digits(buf + i, decimal.digits, decimal.n_digits);
+ } else {
+ assert(metrics.point_loc == EXESS_POINT_BETWEEN);
+ assert(decimal.expt >= -1);
+
+ const size_t n_before = (size_t)decimal.expt + 1u;
+ const size_t n_after = decimal.n_digits - n_before;
+
+ i += copy_digits(buf + i, decimal.digits, n_before);
+ buf[i++] = '.';
+ memcpy(buf + i, decimal.digits + n_before, n_after);
+ i += n_after;
+ }
+
+ return end_write(EXESS_SUCCESS, buf_size, buf, i);
+}
+
+ExessResult
+exess_write_decimal(const double value, const size_t n, char* const buf)
+{
+ const ExessDecimalDouble decimal = exess_measure_double(value);
+
+ return exess_write_decimal_double(decimal, n, buf);
+}
diff --git a/subprojects/exess/src/decimal.h b/subprojects/exess/src/decimal.h
new file mode 100644
index 00000000..7c5aa963
--- /dev/null
+++ b/subprojects/exess/src/decimal.h
@@ -0,0 +1,63 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_DECIMAL_H
+#define EXESS_DECIMAL_H
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+
+// Define C11 numeric constants if the compiler hasn't already
+#ifndef FLT_DECIMAL_DIG
+# define FLT_DECIMAL_DIG 9
+#endif
+#ifndef DBL_DECIMAL_DIG
+# define DBL_DECIMAL_DIG 17
+#endif
+
+typedef enum {
+ EXESS_NEGATIVE,
+ EXESS_NEGATIVE_INFINITY,
+ EXESS_NEGATIVE_ZERO,
+ EXESS_POSITIVE_ZERO,
+ EXESS_POSITIVE,
+ EXESS_POSITIVE_INFINITY,
+ EXESS_NAN,
+} ExessNumberKind;
+
+typedef struct {
+ ExessNumberKind kind; ///< Kind of number
+ int expt; ///< Power of 10 exponent
+ unsigned n_digits; ///< Number of significant digits
+ char digits[DBL_DECIMAL_DIG + 2]; ///< Significant digits
+} ExessDecimalDouble;
+
+ExessDecimalDouble
+exess_measure_decimal(double d, unsigned max_precision);
+
+ExessDecimalDouble
+exess_measure_float(float f);
+
+ExessDecimalDouble
+exess_measure_double(double d);
+
+ExessResult
+exess_write_decimal_double(ExessDecimalDouble decimal,
+ size_t buf_size,
+ char* buf);
+
+#endif // EXESS_DECIMAL_H
diff --git a/subprojects/exess/src/digits.c b/subprojects/exess/src/digits.c
new file mode 100644
index 00000000..dd303269
--- /dev/null
+++ b/subprojects/exess/src/digits.c
@@ -0,0 +1,243 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "digits.h"
+
+#include "bigint.h"
+#include "ieee_float.h"
+#include "soft_float.h"
+#include "warnings.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ This is more or less just an implementation of the classic rational number
+ based floating point print routine ("Dragon4"). See "How to Print
+ Floating-Point Numbers Accurately" by Guy L. Steele Jr. and Jon L White for
+ the canonical source. The basic idea is to find a big rational between 1 and
+ 10 where value = (numer / denom) * 10^e, then continuously divide it to
+ generate decimal digits.
+
+ Unfortunately, this algorithm requires pretty massive bigints to work
+ correctly for all doubles, and isn't particularly fast. Something like
+ Grisu3 could be added to improve performance, but that has the annoying
+ property of needing a more precise fallback in some cases, meaning it would
+ only add more code, not replace any. Since this is already a pretty
+ ridiculous amount of code, I'll hold off on this until it becomes a problem,
+ or somebody comes up with a better algorithm.
+*/
+
+/// Return true if the number is within the lower boundary
+static bool
+within_lower(const ExessBigint* const numer,
+ const ExessBigint* const d_lower,
+ const bool is_even)
+{
+ return is_even ? exess_bigint_compare(numer, d_lower) <= 0
+ : exess_bigint_compare(numer, d_lower) < 0;
+}
+
+/// Return true if the number is within the upper boundary
+static bool
+within_upper(const ExessBigint* const numer,
+ const ExessBigint* const denom,
+ const ExessBigint* const d_upper,
+ const bool is_even)
+{
+ return is_even ? exess_bigint_plus_compare(numer, d_upper, denom) >= 0
+ : exess_bigint_plus_compare(numer, d_upper, denom) > 0;
+}
+
+/**
+ Find values so that 0.1 <= numer/denom < 1 or 1 <= numer/denom < 10.
+
+ @param significand Double significand.
+ @param exponent Double exponent (base 2).
+ @param decimal_power Decimal exponent (log10 of the double).
+ @param[out] numer Numerator of rational number.
+ @param[out] denom Denominator of rational number.
+ @param[out] delta Distance to the lower and upper boundaries.
+*/
+static void
+calculate_initial_values(const uint64_t significand,
+ const int exponent,
+ const int decimal_power,
+ const bool lower_is_closer,
+ ExessBigint* const numer,
+ ExessBigint* const denom,
+ ExessBigint* const delta)
+{
+ /* Use a common denominator of 2^1 so that boundary distance is an integer.
+ If the lower boundary is closer, we need to scale everything but the
+ lower boundary to compensate, so add another factor of two here (this is
+ faster than shifting them again later as in the paper). */
+ const unsigned lg_denom = 1u + lower_is_closer;
+
+ if (exponent >= 0) {
+ // delta = 2^e
+ exess_bigint_set_u32(delta, 1);
+ exess_bigint_shift_left(delta, (unsigned)exponent);
+
+ // numer = f * 2^e
+ exess_bigint_set_u64(numer, significand);
+ exess_bigint_shift_left(numer, (unsigned)exponent + lg_denom);
+
+ // denom = 10^d
+ exess_bigint_set_pow10(denom, (unsigned)decimal_power);
+ exess_bigint_shift_left(denom, lg_denom);
+ } else if (decimal_power >= 0) {
+ // delta = 2^e, which is just 1 here since 2^-e is in the denominator
+ exess_bigint_set_u32(delta, 1);
+
+ // numer = f
+ exess_bigint_set_u64(numer, significand);
+ exess_bigint_shift_left(numer, lg_denom);
+
+ // denom = 10^d * 2^-e
+ exess_bigint_set_pow10(denom, (unsigned)decimal_power);
+ exess_bigint_shift_left(denom, (unsigned)-exponent + lg_denom);
+ } else {
+ // delta = 10^d
+ exess_bigint_set_pow10(delta, (unsigned)-decimal_power);
+
+ // numer = f * 10^-d
+ exess_bigint_set(numer, delta);
+ exess_bigint_multiply_u64(numer, significand);
+ exess_bigint_shift_left(numer, lg_denom);
+
+ // denom = 2^-exponent
+ exess_bigint_set_u32(denom, 1);
+ exess_bigint_shift_left(denom, (unsigned)-exponent + lg_denom);
+ }
+}
+
+#ifndef NDEBUG
+static bool
+check_initial_values(const ExessBigint* const numer,
+ const ExessBigint* const denom,
+ const ExessBigint* const d_upper)
+{
+ ExessBigint upper = *numer;
+ exess_bigint_add(&upper, d_upper);
+ assert(exess_bigint_compare(&upper, denom) >= 0);
+
+ const uint32_t div = exess_bigint_divmod(&upper, denom);
+ assert(div >= 1 && div < 10);
+ return true;
+}
+#endif
+
+static unsigned
+emit_digits(ExessBigint* const numer,
+ const ExessBigint* const denom,
+ ExessBigint* const d_lower,
+ ExessBigint* const d_upper,
+ const bool is_even,
+ char* const buffer,
+ const size_t max_digits)
+{
+ unsigned length = 0;
+ for (size_t i = 0; i < max_digits; ++i) {
+ // Emit the next digit
+ const uint32_t digit = exess_bigint_divmod(numer, denom);
+ assert(digit <= 9);
+ buffer[length++] = (char)('0' + digit);
+
+ // Check for termination
+ const bool within_low = within_lower(numer, d_lower, is_even);
+ const bool within_high = within_upper(numer, denom, d_upper, is_even);
+ if (!within_low && !within_high) {
+ exess_bigint_multiply_u32(numer, 10);
+ exess_bigint_multiply_u32(d_lower, 10);
+ if (d_lower != d_upper) {
+ exess_bigint_multiply_u32(d_upper, 10);
+ }
+ } else {
+ if (!within_low || (within_high && exess_bigint_plus_compare(
+ numer, numer, denom) >= 0)) {
+ // In high only, or halfway and the next digit is > 5, round up
+ assert(buffer[length - 1] != '9');
+ buffer[length - 1]++;
+ }
+
+ break;
+ }
+ }
+
+ return length;
+}
+
+ExessDigitCount
+exess_digits(const double d, char* const buf, const unsigned max_digits)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ assert(isfinite(d) && fpclassify(d) != FP_ZERO);
+ EXESS_RESTORE_WARNINGS
+
+ const ExessSoftFloat value = soft_float_from_double(d);
+ const int power = (int)lrint(log10(d));
+ const bool is_even = !(value.f & 1u);
+ const bool lower_is_closer = double_lower_boundary_is_closer(d);
+
+ // Calculate initial values so that v = (numer / denom) * 10^power
+ ExessBigint numer;
+ ExessBigint denom;
+ ExessBigint d_lower;
+ calculate_initial_values(
+ value.f, value.e, power, lower_is_closer, &numer, &denom, &d_lower);
+
+ ExessBigint d_upper_storage;
+ ExessBigint* d_upper = NULL;
+ if (lower_is_closer) {
+ // Scale upper boundary to account for the closer lower boundary
+ // (the numerator and denominator were already scaled above)
+ d_upper_storage = d_lower;
+ d_upper = &d_upper_storage;
+ exess_bigint_shift_left(d_upper, 1);
+ } else {
+ d_upper = &d_lower; // Boundaries are the same, reuse the lower
+ }
+
+ // Scale if necessary to make 1 <= (numer + delta) / denom < 10
+ ExessDigitCount count = {0, 0};
+ if (within_upper(&numer, &denom, d_upper, is_even)) {
+ count.expt = power;
+ } else {
+ count.expt = power - 1;
+ exess_bigint_multiply_u32(&numer, 10);
+ exess_bigint_multiply_u32(&d_lower, 10);
+ if (d_upper != &d_lower) {
+ exess_bigint_multiply_u32(d_upper, 10);
+ }
+ }
+
+ // Write digits to output
+ assert(check_initial_values(&numer, &denom, d_upper));
+ count.count =
+ emit_digits(&numer, &denom, &d_lower, d_upper, is_even, buf, max_digits);
+
+ // Trim trailing zeros
+ while (count.count > 1 && buf[count.count - 1] == '0') {
+ buf[--count.count] = 0;
+ }
+
+ buf[count.count] = '\0';
+ return count;
+}
diff --git a/subprojects/exess/src/digits.h b/subprojects/exess/src/digits.h
new file mode 100644
index 00000000..0753a0af
--- /dev/null
+++ b/subprojects/exess/src/digits.h
@@ -0,0 +1,38 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_DIGITS_H
+#define EXESS_DIGITS_H
+
+typedef struct {
+ unsigned count; ///< Number of digits
+ int expt; ///< Power of 10 exponent
+} ExessDigitCount;
+
+/**
+ Write significant digits digits for `d` into `buf`.
+
+ Writes only significant digits, without any leading or trailing zeros. The
+ actual number is given by the exponent in the return value.
+
+ @param d The number to convert to digits, must be finite and non-zero.
+ @param buf The output buffer at least `max_digits` long.
+ @param max_digits The maximum number of digits to write.
+*/
+ExessDigitCount
+exess_digits(double d, char* buf, unsigned max_digits);
+
+#endif // EXESS_DIGITS_H
diff --git a/subprojects/exess/src/double.c b/subprojects/exess/src/double.c
new file mode 100644
index 00000000..2934010d
--- /dev/null
+++ b/subprojects/exess/src/double.c
@@ -0,0 +1,53 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "decimal.h"
+#include "read_utils.h"
+#include "scientific.h"
+#include "strtod.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <string.h>
+
+ExessResult
+exess_read_double(double* const out, const char* const str)
+{
+ *out = (double)NAN;
+
+ const size_t i = skip_whitespace(str);
+ ExessDecimalDouble in = {EXESS_NAN, 0u, 0, {0}};
+ const ExessResult r = parse_double(&in, str + i);
+
+ if (!r.status) {
+ *out = parsed_double_to_double(in);
+ }
+
+ return result(r.status, i + r.count);
+}
+
+ExessResult
+exess_write_double(const double value, const size_t buf_size, char* const buf)
+{
+ const ExessDecimalDouble decimal = exess_measure_double(value);
+ const ExessResult r =
+ buf ? exess_write_scientific(decimal, buf_size, buf)
+ : result(EXESS_SUCCESS, exess_scientific_string_length(decimal));
+
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/duration.c b/subprojects/exess/src/duration.c
new file mode 100644
index 00000000..0a753fd3
--- /dev/null
+++ b/subprojects/exess/src/duration.c
@@ -0,0 +1,322 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "time_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef enum { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } Field;
+
+static ExessStatus
+set_field(ExessDuration* const out,
+ const Field current_field,
+ const Field field,
+ const uint32_t value)
+{
+ if (value >= INT32_MAX) {
+ return EXESS_OUT_OF_RANGE;
+ }
+
+ if (field < current_field) {
+ return EXESS_BAD_ORDER;
+ }
+
+ switch (field) {
+ case YEAR:
+ out->months = (int32_t)(12 * lrint(value));
+ break;
+ case MONTH:
+ out->months = (int32_t)(out->months + lrint(value));
+ break;
+ case DAY:
+ out->seconds = (int32_t)(24 * 60 * 60 * lrint(value));
+ break;
+ case HOUR:
+ out->seconds = (int32_t)(out->seconds + 60 * 60 * lrint(value));
+ break;
+ case MINUTE:
+ out->seconds = (int32_t)(out->seconds + 60 * lrint(value));
+ break;
+ case SECOND:
+ out->seconds = (int32_t)(out->seconds + lrint(value));
+ break;
+ }
+
+ return EXESS_SUCCESS;
+}
+
+static ExessResult
+read_date(ExessDuration* const out, const Field field, const char* const str)
+{
+ uint32_t value = 0;
+ ExessResult r = exess_read_uint(&value, str);
+ if (r.status > EXESS_EXPECTED_END) {
+ return r;
+ }
+
+ size_t i = r.count;
+ switch (str[i]) {
+ case 'Y':
+ if ((r.status = set_field(out, field, YEAR, value))) {
+ return r;
+ }
+
+ ++i;
+ if (str[i] != 'T' && !is_end(str[i])) {
+ r = read_date(out, MONTH, str + i);
+ i += r.count;
+ }
+ break;
+
+ case 'M':
+ if ((r.status = set_field(out, field, MONTH, value))) {
+ return r;
+ }
+
+ ++i;
+ if (str[i] != 'T' && !is_end(str[i])) {
+ r = read_date(out, DAY, str + i);
+ i += r.count;
+ }
+ break;
+
+ case 'D':
+ if ((r.status = set_field(out, field, DAY, value))) {
+ return r;
+ }
+
+ ++i;
+ break;
+
+ default:
+ return result(EXESS_EXPECTED_DATE_TAG, i);
+ }
+
+ return result(r.status, i);
+}
+
+static ExessResult
+read_time(ExessDuration* const out, const Field field, const char* const str)
+{
+ uint32_t value = 0;
+ ExessResult r = exess_read_uint(&value, str);
+ if (r.status > EXESS_EXPECTED_END) {
+ return r;
+ }
+
+ size_t i = r.count;
+ ExessResult next = {EXESS_SUCCESS, 0};
+ switch (str[i]) {
+ case '.': {
+ if (!is_digit(str[++i])) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ uint32_t nanoseconds = 0;
+
+ r = read_nanoseconds(&nanoseconds, str + i);
+ i += r.count;
+
+ if (str[i] != 'S') {
+ return result(EXESS_EXPECTED_TIME_TAG, i);
+ }
+
+ r.status = set_field(out, field, SECOND, value);
+ out->nanoseconds = (int32_t)nanoseconds;
+
+ break;
+ }
+
+ case 'H':
+ r.status = set_field(out, field, HOUR, value);
+ if (!is_end(str[i + 1])) {
+ next = read_time(out, MINUTE, str + i + 1);
+ }
+ break;
+
+ case 'M':
+ r.status = set_field(out, field, MINUTE, value);
+ if (!is_end(str[i + 1])) {
+ next = read_time(out, SECOND, str + i + 1);
+ }
+ break;
+
+ case 'S':
+ r.status = set_field(out, field, SECOND, value);
+ break;
+
+ default:
+ return result(EXESS_EXPECTED_TIME_TAG, i);
+ }
+
+ if (r.status) {
+ return r;
+ }
+
+ return result(next.status, i + 1 + next.count);
+}
+
+ExessResult
+exess_read_duration(ExessDuration* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ size_t i = skip_whitespace(str);
+ bool is_negative = false;
+ if (str[i] == '-') {
+ is_negative = true;
+ ++i;
+ }
+
+ if (str[i] != 'P') {
+ return result(EXESS_EXPECTED_DURATION, i);
+ }
+
+ ++i;
+
+ if (str[i] != 'T') {
+ ExessResult r = read_date(out, YEAR, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+
+ if (!is_end(str[i]) && str[i] != 'T') {
+ return result(EXESS_EXPECTED_TIME_SEP, i);
+ }
+ }
+
+ if (str[i] == 'T') {
+ ++i;
+
+ ExessResult r = read_time(out, HOUR, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+ }
+
+ if (is_negative) {
+ out->months = -out->months;
+ out->seconds = -out->seconds;
+ out->nanoseconds = -out->nanoseconds;
+ }
+
+ return end_read(EXESS_SUCCESS, str, i);
+}
+
+static size_t
+write_int_field(ExessResult* r,
+ const uint32_t value,
+ const char tag,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ if (!r->status) {
+ if (value == 0) {
+ *r = result(EXESS_SUCCESS, 0);
+ } else if (!buf) {
+ *r = exess_write_uint(value, buf_size, buf);
+ ++r->count;
+ } else {
+ *r = exess_write_uint(value, buf_size - i, buf + i);
+ if (!r->status) {
+ buf[i + r->count++] = tag;
+ }
+ }
+ }
+
+ return r->count;
+}
+
+ExessResult
+exess_write_duration(const ExessDuration value,
+ const size_t buf_size,
+ char* const buf)
+{
+ // Write zero as a special case
+ size_t i = 0;
+ if (value.months == 0 && value.seconds == 0 && value.nanoseconds == 0) {
+ i += write_string(3, "P0Y", buf_size, buf, i);
+ return end_write(EXESS_SUCCESS, buf_size, buf, i);
+ }
+
+ if (value.months == INT32_MIN || value.seconds == INT32_MIN) {
+ return end_write(EXESS_OUT_OF_RANGE, buf_size, buf, 0);
+ }
+
+ const bool is_negative =
+ (value.months < 0 || value.seconds < 0 || value.nanoseconds < 0);
+
+ if (is_negative &&
+ (value.months > 0 || value.seconds > 0 || value.nanoseconds > 0)) {
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ // Write duration prefix
+ if (value.months < 0 || value.seconds < 0 || value.nanoseconds < 0) {
+ i += write_string(2, "-P", buf_size, buf, i);
+ } else {
+ i += write_char('P', buf_size, buf, i);
+ }
+
+ const uint32_t abs_years = (uint32_t)(abs(value.months) / 12);
+ const uint32_t abs_months = (uint32_t)(abs(value.months) % 12);
+ const uint32_t abs_days = (uint32_t)(abs(value.seconds) / (24 * 60 * 60));
+ const uint32_t abs_hours = (uint32_t)(abs(value.seconds) / 60 / 60 % 24);
+ const uint32_t abs_minutes = (uint32_t)(abs(value.seconds) / 60 % 60);
+ const uint8_t abs_seconds = (uint8_t)(abs(value.seconds) % 60);
+ const uint32_t abs_nanoseconds = (uint32_t)abs(value.nanoseconds);
+
+ // Write date segments if present
+ ExessResult r = result(EXESS_SUCCESS, 0);
+ i += write_int_field(&r, abs_years, 'Y', buf_size, buf, i);
+ i += write_int_field(&r, abs_months, 'M', buf_size, buf, i);
+ i += write_int_field(&r, abs_days, 'D', buf_size, buf, i);
+
+ // Write time segments if present
+ const bool has_time = abs_hours + abs_minutes + abs_seconds + abs_nanoseconds;
+ if (has_time && !r.status) {
+ i += write_char('T', buf_size, buf, i);
+ i += write_int_field(&r, abs_hours, 'H', buf_size, buf, i);
+ i += write_int_field(&r, abs_minutes, 'M', buf_size, buf, i);
+
+ if (abs_seconds != 0 || abs_nanoseconds != 0) {
+ r = write_digits(abs_seconds, buf_size, buf, i);
+ i += r.count;
+
+ if (!r.status && abs_nanoseconds > 0) {
+ i += write_nanoseconds(abs_nanoseconds, buf_size, buf, i);
+ }
+
+ i += write_char('S', buf_size, buf, i);
+ }
+ }
+
+ return end_write(r.status, buf_size, buf, i);
+}
diff --git a/subprojects/exess/src/exess_config.h b/subprojects/exess/src/exess_config.h
new file mode 100644
index 00000000..4325484a
--- /dev/null
+++ b/subprojects/exess/src/exess_config.h
@@ -0,0 +1,79 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/*
+ Configuration header that defines reasonable defaults at compile time.
+
+ This allows compile-time configuration from the command line (typically via
+ the build system) while still allowing the source to be built without any
+ configuration. The build system can define EXESS_NO_DEFAULT_CONFIG to disable
+ defaults, in which case it must define things like HAVE_FEATURE to enable
+ features. The design here ensures that compiler warnings or
+ include-what-you-use will catch any mistakes.
+*/
+
+#ifndef EXESS_CONFIG_H
+#define EXESS_CONFIG_H
+
+// Define version unconditionally so a warning will catch a mismatch
+#define EXESS_VERSION "0.0.1"
+
+#if !defined(EXESS_NO_DEFAULT_CONFIG)
+
+// GCC and clang: __builtin_clz()
+# ifndef HAVE_BUILTIN_CLZ
+# if defined(__has_builtin)
+# if __has_builtin(__builtin_clz)
+# define HAVE_BUILTIN_CLZ 1
+# else
+# define HAVE_BUILTIN_CLZ 0
+# endif
+# elif defined(__GNUC__)
+# define HAVE_BUILTIN_CLZ 1
+# else
+# define HAVE_BUILTIN_CLZ 0
+# endif
+# endif
+
+// GCC and clang: __builtin_clz()
+# ifndef HAVE_BUILTIN_CLZLL
+# if defined(__has_builtin)
+# if __has_builtin(__builtin_clzll)
+# define HAVE_BUILTIN_CLZLL 1
+# else
+# define HAVE_BUILTIN_CLZLL 0
+# endif
+# elif defined(__GNUC__)
+# define HAVE_BUILTIN_CLZLL 1
+# else
+# define HAVE_BUILTIN_CLZLL 0
+# endif
+# endif
+
+#endif // !defined(EXESS_NO_DEFAULT_CONFIG)
+
+/*
+ Make corresponding USE_FEATURE defines based on the HAVE_FEATURE defines from
+ above or the command line. The code checks for these using #if (not #ifdef),
+ so there will be an undefined warning if it checks for an unknown feature,
+ and this header is always required by any code that checks for features, even
+ if the build system defines them all.
+*/
+
+#define USE_BUILTIN_CLZ HAVE_BUILTIN_CLZ
+#define USE_BUILTIN_CLZLL HAVE_BUILTIN_CLZLL
+
+#endif // EXESS_CONFIG_H
diff --git a/subprojects/exess/src/float.c b/subprojects/exess/src/float.c
new file mode 100644
index 00000000..bcc6d49a
--- /dev/null
+++ b/subprojects/exess/src/float.c
@@ -0,0 +1,44 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "decimal.h"
+#include "read_utils.h"
+#include "scientific.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <string.h>
+
+ExessResult
+exess_read_float(float* const out, const char* const str)
+{
+ double value = (double)NAN;
+ const ExessResult r = exess_read_double(&value, str);
+
+ *out = (float)value;
+
+ return r;
+}
+
+ExessResult
+exess_write_float(const float value, const size_t buf_size, char* const buf)
+{
+ const ExessDecimalDouble decimal = exess_measure_float(value);
+
+ return buf ? exess_write_scientific(decimal, buf_size, buf)
+ : result(EXESS_SUCCESS, exess_scientific_string_length(decimal));
+}
diff --git a/subprojects/exess/src/hex.c b/subprojects/exess/src/hex.c
new file mode 100644
index 00000000..bd9de4d6
--- /dev/null
+++ b/subprojects/exess/src/hex.c
@@ -0,0 +1,131 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <string.h>
+
+/// Hex encoding table
+static const char hex_map[] = "0123456789ABCDEF";
+
+static inline uint8_t
+decode_nibble(const char c)
+{
+ if (is_digit(c)) {
+ return (uint8_t)(c - '0');
+ }
+
+ if (c >= 'A' && c <= 'F') {
+ return (uint8_t)(10 + c - 'A');
+ }
+
+ if (c >= 'a' && c <= 'f') {
+ return (uint8_t)(10 + c - 'a');
+ }
+
+ return UINT8_MAX;
+}
+
+static char
+next_char(const char* const str, size_t* const i)
+{
+ *i += skip_whitespace(str + *i);
+
+ return str[*i];
+}
+
+size_t
+exess_hex_decoded_size(const size_t length)
+{
+ return length / 2;
+}
+
+ExessResult
+exess_read_hex(ExessBlob* const out, const char* const str)
+{
+ uint8_t* const uout = (uint8_t*)out->data;
+ size_t i = 0u;
+ size_t o = 0u;
+
+ while (str[i]) {
+ const char hi_char = next_char(str, &i);
+ if (!hi_char) {
+ break;
+ }
+
+ ++i;
+
+ const uint8_t hi = decode_nibble(hi_char);
+ if (hi == UINT8_MAX) {
+ return result(EXESS_EXPECTED_HEX, i);
+ }
+
+ const char lo_char = next_char(str, &i);
+ if (!lo_char) {
+ return result(EXESS_EXPECTED_HEX, i);
+ }
+
+ ++i;
+
+ const uint8_t lo = decode_nibble(lo_char);
+ if (lo == UINT8_MAX) {
+ return result(EXESS_EXPECTED_HEX, i);
+ }
+
+ if (o >= out->size) {
+ return result(EXESS_NO_SPACE, i);
+ }
+
+ uout[o++] = (uint8_t)(hi << 4u) | lo;
+ }
+
+ out->size = o;
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_write_hex(const size_t data_size,
+ const void* const data,
+ const size_t buf_size,
+ char* const buf)
+{
+ const size_t length = 2 * data_size;
+ if (!buf) {
+ return result(EXESS_SUCCESS, length);
+ }
+
+ if (buf_size < length + 1) {
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ const uint8_t* const in = (const uint8_t*)data;
+ size_t o = 0u;
+
+ for (size_t i = 0; i < data_size; ++i) {
+ const uint8_t hi = (in[i] & 0xF0u) >> 4u;
+ const uint8_t lo = (in[i] & 0x0Fu);
+
+ buf[o++] = hex_map[hi];
+ buf[o++] = hex_map[lo];
+ }
+
+ return end_write(EXESS_SUCCESS, buf_size, buf, o);
+}
diff --git a/subprojects/exess/src/ieee_float.h b/subprojects/exess/src/ieee_float.h
new file mode 100644
index 00000000..5c70bf4b
--- /dev/null
+++ b/subprojects/exess/src/ieee_float.h
@@ -0,0 +1,63 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_IEEE_FLOAT_H
+#define EXESS_IEEE_FLOAT_H
+
+#include <float.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static const unsigned dbl_physical_mant_dig = DBL_MANT_DIG - 1u;
+static const uint64_t dbl_mant_mask = 0x000FFFFFFFFFFFFFull;
+static const uint64_t dbl_expt_mask = 0x7FF0000000000000ul;
+static const uint64_t dbl_hidden_bit = 0x0010000000000000ul;
+static const int dbl_expt_bias = 0x3FF + DBL_MANT_DIG - 1;
+static const int dbl_subnormal_expt = -0x3FF - DBL_MANT_DIG + 2;
+
+/// Return the raw representation of a float
+static inline uint32_t
+float_to_rep(const float d)
+{
+ uint32_t rep = 0;
+ memcpy(&rep, &d, sizeof(rep));
+ return rep;
+}
+
+/// Return the raw representation of a double
+static inline uint64_t
+double_to_rep(const double d)
+{
+ uint64_t rep = 0;
+ memcpy(&rep, &d, sizeof(rep));
+ return rep;
+}
+
+/// Return true if the lower boundary is closer than the upper boundary
+static inline bool
+double_lower_boundary_is_closer(const double d)
+{
+ const uint64_t rep = double_to_rep(d);
+ const uint64_t mant = rep & dbl_mant_mask;
+ const uint64_t expt = rep & dbl_expt_mask;
+ const bool is_subnormal = expt == 0;
+
+ // True when f = 2^(p-1) (except for the smallest normal)
+ return !is_subnormal && mant == 0;
+}
+
+#endif // EXESS_IEEE_FLOAT_H
diff --git a/subprojects/exess/src/int.c b/subprojects/exess/src/int.c
new file mode 100644
index 00000000..232cbcc4
--- /dev/null
+++ b/subprojects/exess/src/int.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_int(int32_t* const out, const char* const str)
+{
+ int64_t long_out = 0;
+ const ExessResult r = exess_read_long(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out < INT32_MIN || long_out > INT32_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (int32_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_int(const int32_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_long(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/int_math.c b/subprojects/exess/src/int_math.c
new file mode 100644
index 00000000..ce263b43
--- /dev/null
+++ b/subprojects/exess/src/int_math.c
@@ -0,0 +1,78 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "int_math.h"
+
+#include "exess_config.h"
+
+#include <assert.h>
+
+unsigned
+exess_clz32(const uint32_t i)
+{
+ assert(i != 0);
+
+#if USE_BUILTIN_CLZ
+ return (unsigned)__builtin_clz(i);
+#else
+ unsigned n = 32u;
+ uint32_t bits = i;
+ for (unsigned s = 16; s > 0; s >>= 1) {
+ const uint32_t left = bits >> s;
+ if (left) {
+ n -= s;
+ bits = left;
+ }
+ }
+ return n - bits;
+#endif
+}
+
+unsigned
+exess_clz64(const uint64_t i)
+{
+ assert(i != 0);
+
+#if USE_BUILTIN_CLZLL
+ return (unsigned)__builtin_clzll(i);
+#else
+ return i & 0xFFFFFFFF00000000 ? exess_clz32((uint32_t)(i >> 32u))
+ : 32u + exess_clz32(i & 0xFFFFFFFF);
+#endif
+}
+
+uint64_t
+exess_ilog2(const uint64_t i)
+{
+ assert(i != 0);
+ return (64u - exess_clz64(i | 1u)) - 1u;
+}
+
+uint64_t
+exess_ilog10(const uint64_t i)
+{
+ // See https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10
+ const uint64_t log2 = exess_ilog2(i);
+ const uint64_t t = (log2 + 1u) * 1233u >> 12u;
+
+ return t - (i < POW10[t]) + (i == 0);
+}
+
+uint8_t
+exess_num_digits(const uint64_t i)
+{
+ return i == 0u ? 1u : (uint8_t)(exess_ilog10(i) + 1u);
+}
diff --git a/subprojects/exess/src/int_math.h b/subprojects/exess/src/int_math.h
new file mode 100644
index 00000000..a4a57140
--- /dev/null
+++ b/subprojects/exess/src/int_math.h
@@ -0,0 +1,72 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_INTMATH_H
+#define EXESS_INTMATH_H
+
+#include "attributes.h"
+
+#include <stdint.h>
+
+static const int uint64_digits10 = 19;
+
+static const uint64_t POW10[] = {1ull,
+ 10ull,
+ 100ull,
+ 1000ull,
+ 10000ull,
+ 100000ull,
+ 1000000ull,
+ 10000000ull,
+ 100000000ull,
+ 1000000000ull,
+ 10000000000ull,
+ 100000000000ull,
+ 1000000000000ull,
+ 10000000000000ull,
+ 100000000000000ull,
+ 1000000000000000ull,
+ 10000000000000000ull,
+ 100000000000000000ull,
+ 1000000000000000000ull,
+ 10000000000000000000ull};
+
+/// Return the number of leading zeros in `i`
+EXESS_I_CONST_FUNC
+unsigned
+exess_clz32(uint32_t i);
+
+/// Return the number of leading zeros in `i`
+EXESS_I_CONST_FUNC
+unsigned
+exess_clz64(uint64_t i);
+
+/// Return the log base 2 of `i`
+EXESS_I_CONST_FUNC
+uint64_t
+exess_ilog2(uint64_t i);
+
+/// Return the log base 10 of `i`
+EXESS_I_CONST_FUNC
+uint64_t
+exess_ilog10(uint64_t i);
+
+/// Return the number of decimal digits required to represent `i`
+EXESS_I_CONST_FUNC
+uint8_t
+exess_num_digits(uint64_t i);
+
+#endif // EXESS_INTMATH_H
diff --git a/subprojects/exess/src/long.c b/subprojects/exess/src/long.c
new file mode 100644
index 00000000..2fbcdf59
--- /dev/null
+++ b/subprojects/exess/src/long.c
@@ -0,0 +1,110 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "int_math.h"
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_long(int64_t* const out, const char* const str)
+{
+ *out = 0;
+
+ // Read leading sign if present
+ size_t i = skip_whitespace(str);
+ int sign = 1;
+ if (str[i] == '-') {
+ sign = -1;
+ ++i;
+ } else if (str[i] == '+') {
+ ++i;
+ }
+
+ // Read digits
+ uint64_t magnitude = 0;
+ ExessResult r = exess_read_ulong(&magnitude, str + i);
+ if (r.status > EXESS_EXPECTED_END) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+
+ if (sign > 0) {
+ if (magnitude > (uint64_t)INT64_MAX) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ *out = (int64_t)magnitude;
+ return end_read(EXESS_SUCCESS, str, i);
+ }
+
+ const uint64_t min_magnitude = (uint64_t)(-(INT64_MIN + 1)) + 1;
+ if (magnitude > min_magnitude) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ if (magnitude == min_magnitude) {
+ *out = INT64_MIN;
+ } else {
+ *out = -(int64_t)magnitude;
+ }
+
+ return end_read(r.status, str, i);
+}
+
+static size_t
+exess_long_string_length(const int64_t value)
+{
+ if (value == INT64_MIN) {
+ return 20;
+ }
+
+ if (value < 0) {
+ return 1u + exess_num_digits((uint64_t)-value);
+ }
+
+ return exess_num_digits((uint64_t)value);
+}
+
+ExessResult
+exess_write_long(const int64_t value, const size_t buf_size, char* const buf)
+{
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_long_string_length(value));
+ }
+
+ if (value == INT64_MIN) {
+ return end_write(
+ EXESS_SUCCESS,
+ buf_size,
+ buf,
+ write_string(20, "-9223372036854775808", buf_size, buf, 0));
+ }
+
+ const bool is_negative = value < 0;
+ const uint64_t abs_value = (uint64_t)(is_negative ? -value : value);
+
+ size_t i = (is_negative) ? write_char('-', buf_size, buf, 0) : 0;
+ ExessResult r = write_digits(abs_value, buf_size, buf, i);
+
+ return end_write(r.status, buf_size, buf, i + r.count);
+}
diff --git a/subprojects/exess/src/macros.h b/subprojects/exess/src/macros.h
new file mode 100644
index 00000000..80ed68f5
--- /dev/null
+++ b/subprojects/exess/src/macros.h
@@ -0,0 +1,31 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_MACROS_H
+#define EXESS_MACROS_H
+
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+#define CLAMP(x, l, h) MAX(l, MIN(h, x))
+
+#define SET_IF(pointer, value) \
+ do { \
+ if (pointer) { \
+ *(pointer) = (value); \
+ } \
+ } while (0)
+
+#endif // EXESS_MACROS_H
diff --git a/subprojects/exess/src/read_utils.c b/subprojects/exess/src/read_utils.c
new file mode 100644
index 00000000..823fe97f
--- /dev/null
+++ b/subprojects/exess/src/read_utils.c
@@ -0,0 +1,51 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+ExessResult
+read_two_digit_number(uint8_t* const out,
+ const uint8_t min_value,
+ const uint8_t max_value,
+ const char* const str)
+{
+ size_t i = 0;
+
+ // Read digits
+ size_t d = 0;
+ for (; d < 2; ++d, ++i) {
+ if (is_digit(str[i])) {
+ *out = (uint8_t)((*out * 10) + (str[i] - '0'));
+ } else {
+ break;
+ }
+ }
+
+ // Ensure there are exactly the expected number of digits
+ if (d != 2) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ // Ensure value is in range
+ if (*out < min_value || *out > max_value) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ return result(EXESS_SUCCESS, i);
+}
diff --git a/subprojects/exess/src/read_utils.h b/subprojects/exess/src/read_utils.h
new file mode 100644
index 00000000..3f7e1f56
--- /dev/null
+++ b/subprojects/exess/src/read_utils.h
@@ -0,0 +1,77 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_READ_UTILS_H
+#define EXESS_READ_UTILS_H
+
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+static inline size_t
+skip_whitespace(const char* const str)
+{
+ size_t i = 0;
+ while (is_space(str[i])) {
+ ++i;
+ }
+
+ return i;
+}
+
+static inline bool
+is_end(const char c)
+{
+ switch (c) {
+ case '\0':
+ case ' ':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ case '\v':
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static inline ExessResult
+result(const ExessStatus status, const size_t count)
+{
+ const ExessResult r = {status, count};
+ return r;
+}
+
+ExessResult
+read_two_digit_number(uint8_t* out,
+ uint8_t min_value,
+ uint8_t max_value,
+ const char* str);
+
+static inline ExessResult
+end_read(const ExessStatus status, const char* str, const size_t i)
+{
+ return result((status || is_end(str[i])) ? status : EXESS_EXPECTED_END, i);
+}
+
+#endif // EXESS_READ_UTILS_H
diff --git a/subprojects/exess/src/scientific.c b/subprojects/exess/src/scientific.c
new file mode 100644
index 00000000..3e441a86
--- /dev/null
+++ b/subprojects/exess/src/scientific.c
@@ -0,0 +1,125 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "scientific.h"
+#include "decimal.h"
+#include "int_math.h"
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+size_t
+exess_scientific_string_length(const ExessDecimalDouble value)
+{
+ switch (value.kind) {
+ case EXESS_NEGATIVE:
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return 4;
+ case EXESS_NEGATIVE_ZERO:
+ return 6;
+ case EXESS_POSITIVE_ZERO:
+ return 5;
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ case EXESS_NAN:
+ return 3;
+ }
+
+ const unsigned n_expt_digits =
+ (unsigned)exess_num_digits((unsigned)abs(value.expt));
+
+ return ((value.kind == EXESS_NEGATIVE) + // Sign
+ value.n_digits + 1 + // Digits and point
+ (value.n_digits <= 1) + // Added '0' after point
+ 1 + // 'E'
+ (value.expt < 0) + // Exponent sign
+ n_expt_digits); // Exponent digits
+}
+
+ExessResult
+exess_write_scientific(const ExessDecimalDouble value,
+ const size_t n,
+ char* const buf)
+{
+ size_t i = 0;
+
+ if (n < 4) {
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ switch (value.kind) {
+ case EXESS_NEGATIVE:
+ buf[i++] = '-';
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return write_special(4, "-INF", n, buf);
+ case EXESS_NEGATIVE_ZERO:
+ return write_special(6, "-0.0E0", n, buf);
+ case EXESS_POSITIVE_ZERO:
+ return write_special(5, "0.0E0", n, buf);
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ return write_special(3, "INF", n, buf);
+ case EXESS_NAN:
+ return write_special(3, "NaN", n, buf);
+ }
+
+ if (n - i <= value.n_digits + 1) {
+ buf[0] = '\0';
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ // Write mantissa, with decimal point after the first (normal form)
+ buf[i++] = value.digits[0];
+ buf[i++] = '.';
+ if (value.n_digits > 1) {
+ memcpy(buf + i, value.digits + 1, value.n_digits - 1);
+ i += value.n_digits - 1;
+ } else {
+ buf[i++] = '0';
+ }
+
+ // Write exponent
+
+ const unsigned n_expt_digits = exess_num_digits((unsigned)abs(value.expt));
+
+ if (n - i <= 1u + (value.expt < 0) + n_expt_digits) {
+ buf[0] = '\0';
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ buf[i++] = 'E';
+ if (value.expt < 0) {
+ buf[i++] = '-';
+ }
+
+ unsigned abs_expt = (unsigned)abs(value.expt);
+ char* s = buf + i + n_expt_digits;
+
+ *s-- = '\0';
+ do {
+ *s-- = (char)('0' + (abs_expt % 10));
+ } while ((abs_expt /= 10) > 0);
+
+ return result(EXESS_SUCCESS, i + n_expt_digits);
+}
diff --git a/subprojects/exess/src/scientific.h b/subprojects/exess/src/scientific.h
new file mode 100644
index 00000000..96c3096a
--- /dev/null
+++ b/subprojects/exess/src/scientific.h
@@ -0,0 +1,35 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_SCIENTIFIC_H
+#define EXESS_SCIENTIFIC_H
+
+#include "decimal.h"
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+
+EXESS_CONST_FUNC
+size_t
+exess_scientific_string_length(ExessDecimalDouble value);
+
+ExessResult
+exess_write_scientific(ExessDecimalDouble value,
+ size_t n,
+ char* EXESS_NONNULL buf);
+
+#endif // EXESS_SCIENTIFIC_H
diff --git a/subprojects/exess/src/short.c b/subprojects/exess/src/short.c
new file mode 100644
index 00000000..9241b75a
--- /dev/null
+++ b/subprojects/exess/src/short.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_short(int16_t* const out, const char* const str)
+{
+ int64_t long_out = 0;
+ const ExessResult r = exess_read_long(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out < INT16_MIN || long_out > INT16_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (int16_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_short(const int16_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_long(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/soft_float.c b/subprojects/exess/src/soft_float.c
new file mode 100644
index 00000000..bea9a176
--- /dev/null
+++ b/subprojects/exess/src/soft_float.c
@@ -0,0 +1,161 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "soft_float.h"
+
+#include "ieee_float.h"
+#include "int_math.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdint.h>
+
+/// 10^k for k = min_dec_expt, min_dec_expt + dec_expt_step, ..., max_dec_expt
+static const ExessSoftFloat soft_pow10[] = {
+ {0xFA8FD5A0081C0288, -1220}, {0xBAAEE17FA23EBF76, -1193},
+ {0x8B16FB203055AC76, -1166}, {0xCF42894A5DCE35EA, -1140},
+ {0x9A6BB0AA55653B2D, -1113}, {0xE61ACF033D1A45DF, -1087},
+ {0xAB70FE17C79AC6CA, -1060}, {0xFF77B1FCBEBCDC4F, -1034},
+ {0xBE5691EF416BD60C, -1007}, {0x8DD01FAD907FFC3C, -980},
+ {0xD3515C2831559A83, -954}, {0x9D71AC8FADA6C9B5, -927},
+ {0xEA9C227723EE8BCB, -901}, {0xAECC49914078536D, -874},
+ {0x823C12795DB6CE57, -847}, {0xC21094364DFB5637, -821},
+ {0x9096EA6F3848984F, -794}, {0xD77485CB25823AC7, -768},
+ {0xA086CFCD97BF97F4, -741}, {0xEF340A98172AACE5, -715},
+ {0xB23867FB2A35B28E, -688}, {0x84C8D4DFD2C63F3B, -661},
+ {0xC5DD44271AD3CDBA, -635}, {0x936B9FCEBB25C996, -608},
+ {0xDBAC6C247D62A584, -582}, {0xA3AB66580D5FDAF6, -555},
+ {0xF3E2F893DEC3F126, -529}, {0xB5B5ADA8AAFF80B8, -502},
+ {0x87625F056C7C4A8B, -475}, {0xC9BCFF6034C13053, -449},
+ {0x964E858C91BA2655, -422}, {0xDFF9772470297EBD, -396},
+ {0xA6DFBD9FB8E5B88F, -369}, {0xF8A95FCF88747D94, -343},
+ {0xB94470938FA89BCF, -316}, {0x8A08F0F8BF0F156B, -289},
+ {0xCDB02555653131B6, -263}, {0x993FE2C6D07B7FAC, -236},
+ {0xE45C10C42A2B3B06, -210}, {0xAA242499697392D3, -183},
+ {0xFD87B5F28300CA0E, -157}, {0xBCE5086492111AEB, -130},
+ {0x8CBCCC096F5088CC, -103}, {0xD1B71758E219652C, -77},
+ {0x9C40000000000000, -50}, {0xE8D4A51000000000, -24},
+ {0xAD78EBC5AC620000, 3}, {0x813F3978F8940984, 30},
+ {0xC097CE7BC90715B3, 56}, {0x8F7E32CE7BEA5C70, 83},
+ {0xD5D238A4ABE98068, 109}, {0x9F4F2726179A2245, 136},
+ {0xED63A231D4C4FB27, 162}, {0xB0DE65388CC8ADA8, 189},
+ {0x83C7088E1AAB65DB, 216}, {0xC45D1DF942711D9A, 242},
+ {0x924D692CA61BE758, 269}, {0xDA01EE641A708DEA, 295},
+ {0xA26DA3999AEF774A, 322}, {0xF209787BB47D6B85, 348},
+ {0xB454E4A179DD1877, 375}, {0x865B86925B9BC5C2, 402},
+ {0xC83553C5C8965D3D, 428}, {0x952AB45CFA97A0B3, 455},
+ {0xDE469FBD99A05FE3, 481}, {0xA59BC234DB398C25, 508},
+ {0xF6C69A72A3989F5C, 534}, {0xB7DCBF5354E9BECE, 561},
+ {0x88FCF317F22241E2, 588}, {0xCC20CE9BD35C78A5, 614},
+ {0x98165AF37B2153DF, 641}, {0xE2A0B5DC971F303A, 667},
+ {0xA8D9D1535CE3B396, 694}, {0xFB9B7CD9A4A7443C, 720},
+ {0xBB764C4CA7A44410, 747}, {0x8BAB8EEFB6409C1A, 774},
+ {0xD01FEF10A657842C, 800}, {0x9B10A4E5E9913129, 827},
+ {0xE7109BFBA19C0C9D, 853}, {0xAC2820D9623BF429, 880},
+ {0x80444B5E7AA7CF85, 907}, {0xBF21E44003ACDD2D, 933},
+ {0x8E679C2F5E44FF8F, 960}, {0xD433179D9C8CB841, 986},
+ {0x9E19DB92B4E31BA9, 1013}, {0xEB96BF6EBADF77D9, 1039},
+ {0xAF87023B9BF0EE6B, 1066}};
+
+ExessSoftFloat
+soft_float_from_double(const double d)
+{
+ assert(d >= 0.0);
+
+ const uint64_t rep = double_to_rep(d);
+ const uint64_t frac = rep & dbl_mant_mask;
+ const int expt = (int)((rep & dbl_expt_mask) >> dbl_physical_mant_dig);
+
+ if (expt == 0) { // Subnormal
+ ExessSoftFloat v = {frac, dbl_subnormal_expt};
+ return v;
+ }
+
+ const ExessSoftFloat v = {frac + dbl_hidden_bit, expt - dbl_expt_bias};
+ return v;
+}
+
+double
+soft_float_to_double(const ExessSoftFloat v)
+{
+ return ldexp((double)v.f, v.e);
+}
+
+ExessSoftFloat
+soft_float_normalize(ExessSoftFloat value)
+{
+ const unsigned amount = exess_clz64(value.f);
+
+ value.f <<= amount;
+ value.e -= (int)amount;
+
+ return value;
+}
+
+ExessSoftFloat
+soft_float_multiply(const ExessSoftFloat lhs, const ExessSoftFloat rhs)
+{
+ static const uint64_t mask = 0xFFFFFFFF;
+ static const uint64_t round = 1u << 31u;
+
+ const uint64_t l0 = lhs.f >> 32u;
+ const uint64_t l1 = lhs.f & mask;
+ const uint64_t r0 = rhs.f >> 32u;
+ const uint64_t r1 = rhs.f & mask;
+ const uint64_t l0r0 = l0 * r0;
+ const uint64_t l1r0 = l1 * r0;
+ const uint64_t l0r1 = l0 * r1;
+ const uint64_t l1r1 = l1 * r1;
+ const uint64_t mid = (l1r1 >> 32u) + (l0r1 & mask) + (l1r0 & mask) + round;
+
+ const ExessSoftFloat r = {l0r0 + (l0r1 >> 32u) + (l1r0 >> 32u) + (mid >> 32u),
+ lhs.e + rhs.e + 64};
+
+ return r;
+}
+
+ExessSoftFloat
+soft_float_exact_pow10(const int expt)
+{
+ static const ExessSoftFloat table[8] = {{0xA000000000000000, -60},
+ {0xC800000000000000, -57},
+ {0xFA00000000000000, -54},
+ {0x9C40000000000000, -50},
+ {0xC350000000000000, -47},
+ {0xF424000000000000, -44},
+ {0x9896800000000000, -40}};
+
+ assert(expt > 0);
+ assert(expt < dec_expt_step);
+ return table[expt - 1];
+}
+
+ExessSoftFloat
+soft_float_pow10_under(const int exponent, int* pow10_exponent)
+{
+ assert(exponent >= min_dec_expt);
+ assert(exponent < max_dec_expt + dec_expt_step);
+
+ const int cache_offset = -min_dec_expt;
+ const int index = (exponent + cache_offset) / dec_expt_step;
+
+ *pow10_exponent = min_dec_expt + index * dec_expt_step;
+
+ assert(*pow10_exponent <= exponent);
+ assert(exponent < *pow10_exponent + dec_expt_step);
+
+ return soft_pow10[index];
+}
diff --git a/subprojects/exess/src/soft_float.h b/subprojects/exess/src/soft_float.h
new file mode 100644
index 00000000..b766ae5e
--- /dev/null
+++ b/subprojects/exess/src/soft_float.h
@@ -0,0 +1,71 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_SOFT_FLOAT_H
+#define EXESS_SOFT_FLOAT_H
+
+#include "attributes.h"
+
+#include <stdint.h>
+
+typedef struct {
+ uint64_t f; ///< Significand
+ int e; ///< Exponent
+} ExessSoftFloat;
+
+static const int min_dec_expt = -348;
+static const int max_dec_expt = 340;
+static const int dec_expt_step = 8;
+
+/// Convert `d` to a soft float
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_from_double(double d);
+
+/// Convert `v` to a double
+double
+soft_float_to_double(ExessSoftFloat v);
+
+/// Normalize `value` so the MSb of its significand is 1
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_normalize(ExessSoftFloat value);
+
+/// Multiply `lhs` by `rhs` and return the result
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_multiply(ExessSoftFloat lhs, ExessSoftFloat rhs);
+
+/// Return exactly 10^e for e in [0...dec_expt_step]
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_exact_pow10(int expt);
+
+/**
+ Return a cached power of 10 with exponent not greater than `max_exponent`.
+
+ Valid only for `max_exponent` values from min_dec_expt to max_dec_expt +
+ dec_expt_step. The returned power's exponent is a multiple of
+ dec_expt_step.
+
+ @param max_exponent Maximum decimal exponent of the result.
+ @param[out] pow10_exponent Set to the decimal exponent of the result.
+ @return A cached power of 10 as a soft float.
+*/
+ExessSoftFloat
+soft_float_pow10_under(int max_exponent, int* pow10_exponent);
+
+#endif // EXESS_SOFT_FLOAT_H
diff --git a/subprojects/exess/src/strerror.c b/subprojects/exess/src/strerror.c
new file mode 100644
index 00000000..b2167cac
--- /dev/null
+++ b/subprojects/exess/src/strerror.c
@@ -0,0 +1,70 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+
+const char*
+exess_strerror(ExessStatus status)
+{
+ switch (status) {
+ case EXESS_SUCCESS:
+ return "Success";
+ case EXESS_EXPECTED_END:
+ return "Expected end of value";
+ case EXESS_EXPECTED_BOOLEAN:
+ return "Expected \"false\", \"true\", \"0\" or \"1\"";
+ case EXESS_EXPECTED_INTEGER:
+ return "Expected an integer value";
+ case EXESS_EXPECTED_DURATION:
+ return "Expected a duration starting with 'P'";
+ case EXESS_EXPECTED_SIGN:
+ return "Expected '-' or '+'";
+ case EXESS_EXPECTED_DIGIT:
+ return "Expected a digit";
+ case EXESS_EXPECTED_COLON:
+ return "Expected ':'";
+ case EXESS_EXPECTED_DASH:
+ return "Expected '-'";
+ case EXESS_EXPECTED_TIME_SEP:
+ return "Expected 'T'";
+ case EXESS_EXPECTED_TIME_TAG:
+ return "Expected 'H', 'M', or 'S'";
+ case EXESS_EXPECTED_DATE_TAG:
+ return "Expected 'Y', 'M', or 'D'";
+ case EXESS_EXPECTED_HEX:
+ return "Expected a hexadecimal character";
+ case EXESS_EXPECTED_BASE64:
+ return "Expected a base64 character";
+ case EXESS_BAD_ORDER:
+ return "Invalid field order";
+ case EXESS_BAD_VALUE:
+ return "Invalid value";
+ case EXESS_OUT_OF_RANGE:
+ return "Value outside valid range";
+ case EXESS_NO_SPACE:
+ return "Insufficient space";
+ case EXESS_WOULD_REDUCE_PRECISION:
+ return "Precision reducing coercion required";
+ case EXESS_WOULD_ROUND:
+ return "Rounding coercion required";
+ case EXESS_WOULD_TRUNCATE:
+ return "Truncating coercion required";
+ case EXESS_UNSUPPORTED:
+ return "Unsupported value";
+ }
+
+ return "Unknown error";
+}
diff --git a/subprojects/exess/src/string_utils.h b/subprojects/exess/src/string_utils.h
new file mode 100644
index 00000000..8acb2ffd
--- /dev/null
+++ b/subprojects/exess/src/string_utils.h
@@ -0,0 +1,74 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_STRING_UTILS_H
+#define EXESS_STRING_UTILS_H
+
+#include <stdbool.h>
+
+/// Return true if `c` lies within [`min`...`max`] (inclusive)
+static inline bool
+in_range(const int c, const int min, const int max)
+{
+ return (c >= min && c <= max);
+}
+
+/// Return true if `c` is a whitespace character
+static inline bool
+is_space(const int c)
+{
+ switch (c) {
+ case ' ':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ case '\v':
+ return true;
+ default:
+ return false;
+ }
+}
+
+/// ALPHA ::= [A-Za-z]
+static inline bool
+is_alpha(const int c)
+{
+ return in_range(c, 'A', 'Z') || in_range(c, 'a', 'z');
+}
+
+/// DIGIT ::= [0-9]
+static inline bool
+is_digit(const int c)
+{
+ return in_range(c, '0', '9');
+}
+
+/// HEXDIG ::= DIGIT | "A" | "B" | "C" | "D" | "E" | "F"
+static inline bool
+is_hexdig(const int c)
+{
+ return is_digit(c) || in_range(c, 'A', 'F');
+}
+
+/// BASE64 ::= ALPHA | DIGIT | "+" | "/" | "="
+static inline bool
+is_base64(const int c)
+{
+ return is_alpha(c) || is_digit(c) || c == '+' || c == '/' || c == '=';
+}
+
+#endif // EXESS_STRING_UTILS_H
diff --git a/subprojects/exess/src/strtod.c b/subprojects/exess/src/strtod.c
new file mode 100644
index 00000000..55a0b082
--- /dev/null
+++ b/subprojects/exess/src/strtod.c
@@ -0,0 +1,405 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "strtod.h"
+#include "bigint.h"
+#include "decimal.h"
+#include "ieee_float.h"
+#include "int_math.h"
+#include "macros.h"
+#include "read_utils.h"
+#include "soft_float.h"
+#include "string_utils.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static inline int
+read_sign(const char** const sptr)
+{
+ if (**sptr == '-') {
+ ++(*sptr);
+ return -1;
+ }
+
+ if (**sptr == '+') {
+ ++(*sptr);
+ }
+
+ return 1;
+}
+
+ExessResult
+parse_decimal(ExessDecimalDouble* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Read leading sign if necessary
+ const char* s = str;
+ const int sign = read_sign(&s);
+ int n_leading_before = 0;
+
+ out->kind = (sign < 0) ? EXESS_NEGATIVE : EXESS_POSITIVE;
+
+ // Check that the first character is valid
+ if (*s != '.' && !is_digit(*s)) {
+ return result(EXESS_EXPECTED_DIGIT, (size_t)(s - str));
+ }
+
+ // Skip leading zeros before decimal point
+ while (*s == '0') {
+ ++s;
+ ++n_leading_before;
+ }
+
+ // Skip leading zeros after decimal point
+ int n_leading_after = 0; // Zeros skipped after decimal point
+ bool after_point = false; // True if we are after the decimal point
+ if (*s == '.') {
+ after_point = true;
+ for (++s; *s == '0'; ++s) {
+ ++n_leading_after;
+ }
+ }
+
+ // Read significant digits of the mantissa into a 64-bit integer
+ uint64_t frac = 0; // Fraction value (ignoring decimal point)
+ int n_before = 0; // Number of digits before decimal point
+ int n_after = 0; // Number of digits after decimal point
+ for (; out->n_digits < DBL_DECIMAL_DIG + 1; ++s) {
+ if (is_digit(*s)) {
+ frac = (frac * 10) + (unsigned)(*s - '0');
+ n_before += !after_point;
+ n_after += after_point;
+ out->digits[out->n_digits++] = *s;
+ } else if (*s == '.' && !after_point) {
+ after_point = true;
+ } else {
+ break;
+ }
+ }
+
+ // Skip extra digits
+ int n_extra_before = 0;
+ int n_extra_after = 0;
+ for (;; ++s) {
+ if (*s == '.' && !after_point) {
+ after_point = true;
+ } else if (is_digit(*s)) {
+ n_extra_before += !after_point;
+ n_extra_after += after_point;
+ } else {
+ break;
+ }
+ }
+
+ // Calculate final output exponent
+ out->expt = n_extra_before - n_after - n_leading_after;
+
+ if (out->n_digits == 0) {
+ out->kind =
+ out->kind == EXESS_NEGATIVE ? EXESS_NEGATIVE_ZERO : EXESS_POSITIVE_ZERO;
+ }
+
+ return result(EXESS_SUCCESS, (size_t)(s - str));
+}
+
+ExessResult
+parse_double(ExessDecimalDouble* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Handle non-numeric special cases
+
+ if (!strcmp(str, "NaN")) {
+ out->kind = EXESS_NAN;
+ return result(EXESS_SUCCESS, 3u);
+ }
+
+ if (!strcmp(str, "-INF")) {
+ out->kind = EXESS_NEGATIVE_INFINITY;
+ return result(EXESS_SUCCESS, 4u);
+ }
+
+ if (!strcmp(str, "INF")) {
+ out->kind = EXESS_POSITIVE_INFINITY;
+ return result(EXESS_SUCCESS, 3u);
+ }
+
+ if (!strcmp(str, "+INF")) {
+ out->kind = EXESS_POSITIVE_INFINITY;
+ return result(EXESS_SUCCESS, 4u);
+ }
+
+ // Read mantissa as a decimal
+ const ExessResult r = parse_decimal(out, str);
+ if (r.status) {
+ return r;
+ }
+
+ const char* s = str + r.count;
+
+ // Read exponent
+ int abs_expt = 0;
+ int expt_sign = 1;
+ if (*s == 'e' || *s == 'E') {
+ ++s;
+
+ if (*s != '-' && *s != '+' && !is_digit(*s)) {
+ return result(EXESS_EXPECTED_DIGIT, (size_t)(s - str));
+ }
+
+ expt_sign = read_sign(&s);
+ while (is_digit(*s)) {
+ abs_expt = (abs_expt * 10) + (*s++ - '0');
+ }
+ }
+
+ // Calculate final output exponent
+ out->expt += expt_sign * abs_expt;
+
+ if (out->n_digits == 0) {
+ out->kind = out->kind < EXESS_POSITIVE_ZERO ? EXESS_NEGATIVE_ZERO
+ : EXESS_POSITIVE_ZERO;
+ }
+
+ return result(EXESS_SUCCESS, (size_t)(s - str));
+}
+
+static uint64_t
+normalize(ExessSoftFloat* value, const uint64_t error)
+{
+ const int original_e = value->e;
+
+ *value = soft_float_normalize(*value);
+
+ assert(value->e <= original_e);
+ return error << (unsigned)(original_e - value->e);
+}
+
+/**
+ Return the error added by floating point multiplication.
+
+ Should be l + r + l*r/(2^64) + 0.5, but we short the denominator to 63 due
+ to lack of precision, which effectively rounds up.
+*/
+static inline uint64_t
+product_error(const uint64_t lerror,
+ const uint64_t rerror,
+ const uint64_t half_ulp)
+{
+ return lerror + rerror + ((lerror * rerror) >> 63u) + half_ulp;
+}
+
+/**
+ Guess the binary floating point value for decimal input.
+
+ @param significand Significand from the input.
+ @param expt10 Decimal exponent from the input.
+ @param n_digits Number of decimal digits in the significand.
+ @param[out] guess Either the exact number, or its predecessor.
+ @return True if `guess` is correct.
+*/
+static bool
+sftod(const uint64_t significand,
+ const int expt10,
+ const int n_digits,
+ ExessSoftFloat* const guess)
+{
+ assert(expt10 <= max_dec_expt);
+ assert(expt10 >= min_dec_expt);
+
+ /* The general idea here is to try and find a power of 10 that we can
+ multiply by the significand to get the number. We get one from the
+ cache which is possibly too small, then multiply by another power of 10
+ to make up the difference if necessary. For example, with a target
+ power of 10^70, if we get 10^68 from the cache, then we multiply again
+ by 10^2. This, as well as normalization, accumulates error, which is
+ tracked throughout to know if we got the precise number. */
+
+ // Use a common denominator of 2^3 to avoid fractions
+ static const unsigned lg_denom = 3;
+ static const uint64_t denom = 1u << 3u;
+ static const uint64_t half_ulp = 4u;
+
+ // Start out with just the significand, and no error
+ ExessSoftFloat input = {significand, 0};
+ uint64_t error = normalize(&input, 0);
+
+ // Get a power of 10 that takes us most of the way without overshooting
+ int cached_expt10 = 0;
+ ExessSoftFloat pow10 = soft_float_pow10_under(expt10, &cached_expt10);
+
+ // Get an exact fixup power if necessary
+ const int d_expt10 = expt10 - cached_expt10;
+ if (d_expt10) {
+ input = soft_float_multiply(input, soft_float_exact_pow10(d_expt10));
+ if (d_expt10 > uint64_digits10 - n_digits) {
+ error += half_ulp; // Product does not fit in an integer
+ }
+ }
+
+ // Multiply the significand by the power, normalize, and update the error
+ input = soft_float_multiply(input, pow10);
+ error = normalize(&input, product_error(error, half_ulp, half_ulp));
+
+ // Get the effective number of significant bits from the order of magnitude
+ const int magnitude = 64 + input.e;
+ const int real_magnitude = magnitude - dbl_subnormal_expt;
+ const unsigned n_significant_bits =
+ (unsigned)MAX(0, MIN(real_magnitude, DBL_MANT_DIG));
+
+ // Calculate the number of "extra" bits of precision we have
+ assert(n_significant_bits <= 64);
+ unsigned n_extra_bits = 64u - n_significant_bits;
+ if (n_extra_bits + lg_denom >= 64u) {
+ // Very small subnormal where extra * denom does not fit in an integer
+ // Shift right (and accumulate some more error) to compensate
+ const unsigned amount = (n_extra_bits + lg_denom) - 63;
+
+ input.f >>= amount;
+ input.e += (int)amount;
+ error = product_error((error >> amount) + 1u, half_ulp, half_ulp);
+ n_extra_bits -= amount;
+ }
+
+ // Calculate boundaries for the extra bits (with the common denominator)
+ assert(n_extra_bits < 64);
+ const uint64_t extra_mask = (1ull << n_extra_bits) - 1u;
+ const uint64_t extra_bits = (input.f & extra_mask) * denom;
+ const uint64_t middle = (1ull << (n_extra_bits - 1u)) * denom;
+ const uint64_t low = middle - error;
+ const uint64_t high = middle + error;
+
+ // Round to nearest representable double
+ guess->f = (input.f >> n_extra_bits) + (extra_bits >= high);
+ guess->e = input.e + (int)n_extra_bits;
+
+ // Too inaccurate if the extra bits are within the error around the middle
+ return extra_bits <= low || extra_bits >= high;
+}
+
+static int
+compare_buffer(const char* buf, const int expt, const ExessSoftFloat upper)
+{
+ ExessBigint buf_bigint;
+ exess_bigint_set_decimal_string(&buf_bigint, buf);
+
+ ExessBigint upper_bigint;
+ exess_bigint_set_u64(&upper_bigint, upper.f);
+
+ if (expt >= 0) {
+ exess_bigint_multiply_pow10(&buf_bigint, (unsigned)expt);
+ } else {
+ exess_bigint_multiply_pow10(&upper_bigint, (unsigned)-expt);
+ }
+
+ if (upper.e > 0) {
+ exess_bigint_shift_left(&upper_bigint, (unsigned)upper.e);
+ } else {
+ exess_bigint_shift_left(&buf_bigint, (unsigned)-upper.e);
+ }
+
+ return exess_bigint_compare(&buf_bigint, &upper_bigint);
+}
+
+double
+parsed_double_to_double(const ExessDecimalDouble in)
+{
+ static const int n_exact_pow10 = sizeof(POW10) / sizeof(POW10[0]);
+ static const unsigned max_exact_int_digits = 15; // Digits that fit exactly
+ static const int max_decimal_power = 309; // Max finite power
+ static const int min_decimal_power = -324; // Min non-zero power
+
+ switch (in.kind) {
+ case EXESS_NEGATIVE:
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return (double)-INFINITY;
+ case EXESS_NEGATIVE_ZERO:
+ return -0.0;
+ case EXESS_POSITIVE_ZERO:
+ return 0.0;
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ return (double)INFINITY;
+ case EXESS_NAN:
+ return (double)NAN;
+ }
+
+ uint64_t frac = 0;
+ for (unsigned i = 0u; i < in.n_digits; ++i) {
+ if (is_digit(in.digits[i])) {
+ frac = (frac * 10) + (unsigned)(in.digits[i] - '0');
+ }
+ }
+
+ const int expt = in.expt;
+ const int result_power = (int)in.n_digits + expt;
+
+ // Return early for simple exact cases
+
+ const int sign = in.kind >= EXESS_POSITIVE_ZERO ? 1 : -1;
+
+ if (result_power > max_decimal_power) {
+ return sign * (double)INFINITY;
+ }
+
+ if (result_power < min_decimal_power) {
+ return sign * 0.0;
+ }
+
+ if (in.n_digits < max_exact_int_digits) {
+ if (expt < 0 && -expt < n_exact_pow10) {
+ return sign * ((double)frac / (double)POW10[-expt]);
+ }
+
+ if (expt >= 0 && expt < n_exact_pow10) {
+ return sign * ((double)frac * (double)POW10[expt]);
+ }
+ }
+
+ // Try to guess the number using only soft floating point (fast path)
+ ExessSoftFloat guess = {0, 0};
+ const bool exact = sftod(frac, expt, (int)in.n_digits, &guess);
+ const double g = soft_float_to_double(guess);
+ if (exact) {
+ return sign * g;
+ }
+
+ // Not sure, guess is either the number or its predecessor (rare slow path)
+ // Compare it with the buffer using bigints to find out which
+ const ExessSoftFloat upper = {guess.f * 2 + 1, guess.e - 1};
+ const int cmp = compare_buffer(in.digits, in.expt, upper);
+ if (cmp < 0) {
+ return sign * g;
+ }
+
+ if (cmp > 0) {
+ return sign * nextafter(g, (double)INFINITY);
+ }
+
+ if ((guess.f & 1u) == 0) {
+ return sign * g; // Round towards even
+ }
+
+ return sign * nextafter(g, (double)INFINITY); // Round odd up
+}
diff --git a/subprojects/exess/src/strtod.h b/subprojects/exess/src/strtod.h
new file mode 100644
index 00000000..7fdf7d6a
--- /dev/null
+++ b/subprojects/exess/src/strtod.h
@@ -0,0 +1,33 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_STRTOD_H
+#define EXESS_STRTOD_H
+
+#include "decimal.h"
+
+#include "exess/exess.h"
+
+ExessResult
+parse_decimal(ExessDecimalDouble* out, const char* str);
+
+ExessResult
+parse_double(ExessDecimalDouble* out, const char* str);
+
+double
+parsed_double_to_double(ExessDecimalDouble in);
+
+#endif // EXESS_STRTOD_H
diff --git a/subprojects/exess/src/time.c b/subprojects/exess/src/time.c
new file mode 100644
index 00000000..8c7bc12c
--- /dev/null
+++ b/subprojects/exess/src/time.c
@@ -0,0 +1,172 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "time_utils.h"
+#include "timezone.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+ExessResult
+read_nanoseconds(uint32_t* const out, const char* const str)
+{
+ size_t i = 0;
+ char frac_digits[10] = {'0', '0', '0', '0', '0', '0', '0', '0', '0', 0};
+ for (unsigned j = 0u; j < 9u && is_digit(str[i]); ++j) {
+ frac_digits[j] = str[i++];
+ }
+
+ return result(exess_read_uint(out, frac_digits).status, i);
+}
+
+ExessResult
+exess_read_time(ExessTime* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Read hour
+ size_t i = skip_whitespace(str);
+ ExessResult r = read_two_digit_number(&out->hour, 0, 24, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read hour-minute delimiter
+ i += r.count;
+ if (str[i] != ':') {
+ return result(EXESS_EXPECTED_COLON, i);
+ }
+
+ // Read minute
+ ++i;
+ r = read_two_digit_number(&out->minute, 0, 59, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read minute-second delimiter
+ i += r.count;
+ if (str[i] != ':') {
+ return result(EXESS_EXPECTED_COLON, i);
+ }
+
+ // Read second
+ ++i;
+ r = read_two_digit_number(&out->second, 0, 59, str + i);
+ i += r.count;
+ if (r.status) {
+ return result(r.status, i);
+ }
+
+ // Read nanoseconds if present
+ if (str[i] == '.') {
+ ++i;
+ r = read_nanoseconds(&out->nanosecond, str + i);
+ i += r.count;
+ }
+
+ // Read timezone if present
+ if (!is_end(str[i])) {
+ r = exess_read_timezone(&out->zone, str + i);
+ i += r.count;
+ } else {
+ out->zone.quarter_hours = EXESS_LOCAL;
+ }
+
+ return end_read(r.status, str, i);
+}
+
+size_t
+write_nanoseconds(const uint32_t nanosecond,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ assert(nanosecond <= 999999999);
+
+ if (nanosecond == 0) {
+ return 0;
+ }
+
+ char frac_digits[10] = {'0', '0', '0', '0', '0', '0', '0', '0', '0', 0};
+
+ // Write digits right to left, but replace trailing zeros with null
+ uint32_t remaining = nanosecond;
+ uint32_t n_trailing = 0;
+ bool wrote_nonzero = false;
+ for (uint32_t j = 0; remaining > 0; ++j) {
+ const char digit = (char)('0' + (remaining % 10));
+ if (!wrote_nonzero && digit == '0') {
+ frac_digits[8 - j] = '\0';
+ ++n_trailing;
+ } else {
+ frac_digits[8 - j] = digit;
+ }
+
+ wrote_nonzero = wrote_nonzero || digit != '0';
+ remaining /= 10;
+ }
+
+ size_t n = write_char('.', buf_size, buf, i);
+
+ n += write_string(9 - n_trailing, frac_digits, buf_size, buf, i + n);
+
+ return n;
+}
+
+ExessResult
+write_time(const ExessTime value,
+ const size_t buf_size,
+ char* const buf,
+ const size_t offset)
+{
+ if (value.hour > 24 || value.minute > 59 || value.second > 59 ||
+ value.nanosecond > 999999999 ||
+ (value.hour == 24 &&
+ (value.minute != 0 || value.second != 0 || value.nanosecond != 0))) {
+ return result(EXESS_BAD_VALUE, 0);
+ }
+
+ size_t o = offset;
+
+ // Write integral hour, minute, and second
+ o += write_two_digit_number(value.hour, buf_size, buf, o);
+ o += write_char(':', buf_size, buf, o);
+ o += write_two_digit_number(value.minute, buf_size, buf, o);
+ o += write_char(':', buf_size, buf, o);
+ o += write_two_digit_number(value.second, buf_size, buf, o);
+ o += write_nanoseconds(value.nanosecond, buf_size, buf, o);
+
+ const ExessResult r = write_timezone(value.zone, buf_size, buf, o);
+
+ return result(r.status, o - offset + r.count);
+}
+
+ExessResult
+exess_write_time(const ExessTime value, const size_t buf_size, char* const buf)
+{
+ const ExessResult r = write_time(value, buf_size, buf, 0);
+
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/time_utils.h b/subprojects/exess/src/time_utils.h
new file mode 100644
index 00000000..32e4b6df
--- /dev/null
+++ b/subprojects/exess/src/time_utils.h
@@ -0,0 +1,35 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_TIME_UTILS_H
+#define EXESS_TIME_UTILS_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/// Read fractional digits as an integer number of nanoseconds
+ExessResult
+read_nanoseconds(uint32_t* out, const char* str);
+
+/// Write nanoseconds as fractional digits
+size_t
+write_nanoseconds(uint32_t nanosecond, size_t buf_size, char* buf, size_t i);
+
+/// Write a complete time with timezone suffix if necessary
+ExessResult
+write_time(ExessTime value, size_t buf_size, char* buf, size_t offset);
+
+#endif // EXESS_TIME_UTILS_H
diff --git a/subprojects/exess/src/timezone.c b/subprojects/exess/src/timezone.c
new file mode 100644
index 00000000..717f0d08
--- /dev/null
+++ b/subprojects/exess/src/timezone.c
@@ -0,0 +1,150 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "timezone.h"
+#include "date_utils.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_timezone(ExessTimezone* const out, const char* const str)
+{
+ out->quarter_hours = INT8_MAX;
+
+ // Start at the beginning (no whitespace skipping here)
+ size_t i = 0;
+
+ // Handle UTC special case
+ if (str[i] == 'Z') {
+ out->quarter_hours = 0;
+ return result(EXESS_SUCCESS, i + 1);
+ }
+
+ // Read leading sign (required)
+ int sign = 1;
+ switch (str[i]) {
+ case '+':
+ ++i;
+ break;
+ case '-':
+ sign = -1;
+ ++i;
+ break;
+ default:
+ return result(EXESS_EXPECTED_SIGN, i);
+ }
+
+ const char h0 = str[i];
+ if (!is_digit(h0)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ const char h1 = str[++i];
+ if (!is_digit(h1)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ ++i;
+
+ const int8_t hour = (int8_t)(sign * (10 * (h0 - '0') + (h1 - '0')));
+ if (hour > 14 || hour < -14) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ if (str[i] != ':') {
+ return result(EXESS_EXPECTED_COLON, i);
+ }
+
+ const char m0 = str[++i];
+ if (!is_digit(m0)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ const char m1 = str[++i];
+ if (!is_digit(m1)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ const int8_t minute = (int8_t)(sign * (10 * (m0 - '0') + (m1 - '0')));
+
+ ++i;
+
+ if (minute % 15) {
+ return result(EXESS_UNSUPPORTED, i);
+ }
+
+ if (minute > 59 || minute < -59) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ const int8_t quarters = (int8_t)(4 * hour + minute / 15);
+ if (quarters < -56 || quarters > 56) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ out->quarter_hours = quarters;
+
+ return result(EXESS_SUCCESS, i);
+}
+
+size_t
+exess_timezone_string_length(const ExessTimezone value)
+{
+ return value.quarter_hours != EXESS_LOCAL ? (value.quarter_hours == 0) ? 1 : 6
+ : 0;
+}
+
+ExessResult
+write_timezone(const ExessTimezone value,
+ const size_t buf_size,
+ char* const buf,
+ size_t o)
+{
+ if (value.quarter_hours == EXESS_LOCAL) {
+ return result(EXESS_SUCCESS, 0);
+ }
+
+ if (value.quarter_hours < -56 || value.quarter_hours > 56) {
+ return result(EXESS_BAD_VALUE, 0);
+ }
+
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_timezone_string_length(value));
+ }
+
+ if (value.quarter_hours == 0) {
+ write_char('Z', buf_size, buf, o);
+ return result(EXESS_SUCCESS, 1);
+ }
+
+ const uint8_t abs_quarters = (uint8_t)abs(value.quarter_hours);
+ const uint8_t abs_hour = abs_quarters / 4;
+ const uint8_t abs_minute = (uint8_t)(15u * (abs_quarters % 4u));
+
+ size_t n = 0;
+ n += write_char(value.quarter_hours < 0 ? '-' : '+', buf_size, buf, o + n);
+ n += write_two_digit_number(abs_hour, buf_size, buf, o + n);
+ n += write_char(':', buf_size, buf, o + n);
+ n += write_two_digit_number(abs_minute, buf_size, buf, o + n);
+
+ return result(EXESS_SUCCESS, n);
+}
diff --git a/subprojects/exess/src/timezone.h b/subprojects/exess/src/timezone.h
new file mode 100644
index 00000000..b10c2483
--- /dev/null
+++ b/subprojects/exess/src/timezone.h
@@ -0,0 +1,59 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_TIMEZONE_H
+#define EXESS_TIMEZONE_H
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+
+/// The maximum length of a canonical timezone string, 6
+#define EXESS_MAX_TIMEZONE_LENGTH 6
+
+/**
+ Read a timezone string after any leading whitespace.
+
+ @param out Set to the parsed value, or false on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_timezone(ExessTimezone* EXESS_NONNULL out,
+ const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical timezone suffix.
+
+ The output is always in canonical form, either `Z` for UTC or a signed hour
+ and minute offset with leading zeros, like `-05:30` or `+14:00`.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+ @param o The current write offset in `buf`
+
+ @return #EXESS_SUCCESS on success, #EXESS_NO_SPACE if the buffer is too
+ small, or #EXESS_BAD_VALUE if the value is invalid.
+*/
+ExessResult
+write_timezone(ExessTimezone value,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf,
+ size_t o);
+
+#endif // EXESS_TIMEZONE_H
diff --git a/subprojects/exess/src/ubyte.c b/subprojects/exess/src/ubyte.c
new file mode 100644
index 00000000..f520832a
--- /dev/null
+++ b/subprojects/exess/src/ubyte.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_ubyte(uint8_t* const out, const char* const str)
+{
+ uint64_t long_out = 0;
+ const ExessResult r = exess_read_ulong(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out > UINT8_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (uint8_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_ubyte(const uint8_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_ulong(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/uint.c b/subprojects/exess/src/uint.c
new file mode 100644
index 00000000..dad46b48
--- /dev/null
+++ b/subprojects/exess/src/uint.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_uint(uint32_t* const out, const char* const str)
+{
+ uint64_t long_out = 0;
+ const ExessResult r = exess_read_ulong(&long_out, str);
+ if (r.status && r.status != EXESS_EXPECTED_END) {
+ return r;
+ }
+
+ if (long_out > UINT32_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (uint32_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_uint(const uint32_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_ulong(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/ulong.c b/subprojects/exess/src/ulong.c
new file mode 100644
index 00000000..3d9e00c9
--- /dev/null
+++ b/subprojects/exess/src/ulong.c
@@ -0,0 +1,94 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+#include "int_math.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_ulong(uint64_t* const out, const char* const str)
+{
+ *out = 0;
+
+ // Ensure the first character is a digit
+ size_t i = skip_whitespace(str);
+ if (!is_digit(str[i])) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ // Skip leading zeros
+ int n_zeroes = 0;
+ while (str[i] == '0') {
+ ++i;
+ ++n_zeroes;
+ }
+
+ // Read digits
+ for (; is_digit(str[i]); ++i) {
+ const uint64_t next = (*out * 10u) + (uint64_t)(str[i] - '0');
+ if (next < *out) {
+ *out = 0;
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ *out = next;
+ }
+
+ return end_read(EXESS_SUCCESS, str, i);
+}
+
+ExessResult
+write_digits(const uint64_t value,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ const uint8_t n_digits = exess_num_digits(value);
+ if (!buf) {
+ return result(EXESS_SUCCESS, n_digits);
+ }
+
+ if (i + n_digits >= buf_size) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ // Point s to the end
+ char* s = buf + i + n_digits - 1u;
+
+ // Write integer part (right to left)
+ uint64_t remaining = value;
+ do {
+ *s-- = (char)('0' + (remaining % 10));
+ } while ((remaining /= 10) > 0);
+
+ return result(EXESS_SUCCESS, n_digits);
+}
+
+ExessResult
+exess_write_ulong(const uint64_t value, const size_t buf_size, char* const buf)
+{
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_num_digits(value));
+ }
+
+ const ExessResult r = write_digits(value, buf_size, buf, 0);
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/ushort.c b/subprojects/exess/src/ushort.c
new file mode 100644
index 00000000..fc4d2c7e
--- /dev/null
+++ b/subprojects/exess/src/ushort.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_ushort(uint16_t* const out, const char* const str)
+{
+ uint64_t long_out = 0;
+ const ExessResult r = exess_read_ulong(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out > UINT16_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (uint16_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_ushort(const uint16_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_ulong(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/variant.c b/subprojects/exess/src/variant.c
new file mode 100644
index 00000000..d8977abd
--- /dev/null
+++ b/subprojects/exess/src/variant.c
@@ -0,0 +1,431 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+// Constructors
+
+ExessVariant
+exess_make_nothing(const ExessStatus status)
+{
+ ExessVariant v = {EXESS_NOTHING, .value.as_status = status};
+ return v;
+}
+
+ExessVariant
+exess_make_boolean(const bool value)
+{
+ ExessVariant v = {EXESS_BOOLEAN, .value.as_bool = value};
+ return v;
+}
+
+ExessVariant
+exess_make_decimal(const double value)
+{
+ ExessVariant v = {EXESS_DECIMAL, .value.as_double = value};
+ return v;
+}
+
+ExessVariant
+exess_make_double(const double value)
+{
+ ExessVariant v = {EXESS_DOUBLE, .value.as_double = value};
+ return v;
+}
+
+ExessVariant
+exess_make_float(const float value)
+{
+ ExessVariant v = {EXESS_FLOAT, .value.as_float = value};
+ return v;
+}
+
+ExessVariant
+exess_make_long(const int64_t value)
+{
+ ExessVariant v = {EXESS_LONG, .value.as_long = value};
+ return v;
+}
+
+ExessVariant
+exess_make_int(const int32_t value)
+{
+ ExessVariant v = {EXESS_INT, .value.as_int = value};
+ return v;
+}
+
+ExessVariant
+exess_make_short(const int16_t value)
+{
+ ExessVariant v = {EXESS_SHORT, .value.as_short = value};
+ return v;
+}
+
+ExessVariant
+exess_make_byte(const int8_t value)
+{
+ ExessVariant v = {EXESS_BYTE, .value.as_byte = value};
+ return v;
+}
+
+ExessVariant
+exess_make_ulong(const uint64_t value)
+{
+ ExessVariant v = {EXESS_ULONG, .value.as_ulong = value};
+ return v;
+}
+
+ExessVariant
+exess_make_uint(const uint32_t value)
+{
+ ExessVariant v = {EXESS_UINT, .value.as_uint = value};
+ return v;
+}
+
+ExessVariant
+exess_make_ushort(const uint16_t value)
+{
+ ExessVariant v = {EXESS_USHORT, .value.as_ushort = value};
+ return v;
+}
+
+ExessVariant
+exess_make_ubyte(const uint8_t value)
+{
+ ExessVariant v = {EXESS_UBYTE, .value.as_ubyte = value};
+ return v;
+}
+
+ExessVariant
+exess_make_duration(const ExessDuration value)
+{
+ ExessVariant v = {EXESS_DURATION, .value.as_duration = value};
+ return v;
+}
+
+ExessVariant
+exess_make_datetime(const ExessDateTime value)
+{
+ ExessVariant v = {EXESS_DATETIME, .value.as_datetime = value};
+ return v;
+}
+
+ExessVariant
+exess_make_time(const ExessTime value)
+{
+ ExessVariant v = {EXESS_TIME, .value.as_time = value};
+ return v;
+}
+
+ExessVariant
+exess_make_date(const ExessDate value)
+{
+ ExessVariant v = {EXESS_DATE, .value.as_date = value};
+ return v;
+}
+
+ExessVariant
+exess_make_hex(const ExessBlob blob)
+{
+ ExessVariant v = {EXESS_HEX, .value.as_blob = blob};
+ return v;
+}
+
+ExessVariant
+exess_make_base64(const ExessBlob blob)
+{
+ ExessVariant v = {EXESS_BASE64, .value.as_blob = blob};
+ return v;
+}
+
+// Accessors
+
+ExessStatus
+exess_get_status(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_NOTHING ? variant->value.as_status
+ : EXESS_SUCCESS;
+}
+const bool*
+exess_get_boolean(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_BOOLEAN ? &variant->value.as_bool : NULL;
+}
+
+const double*
+exess_get_double(const ExessVariant* const variant)
+{
+ return (variant->datatype == EXESS_DECIMAL ||
+ variant->datatype == EXESS_DOUBLE)
+ ? &variant->value.as_double
+ : NULL;
+}
+
+const float*
+exess_get_float(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_FLOAT ? &variant->value.as_float : NULL;
+}
+
+const int64_t*
+exess_get_long(const ExessVariant* const variant)
+{
+ return (variant->datatype >= EXESS_INTEGER && variant->datatype <= EXESS_LONG)
+ ? &variant->value.as_long
+ : NULL;
+}
+
+const int32_t*
+exess_get_int(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_INT ? &variant->value.as_int : NULL;
+}
+
+const int16_t*
+exess_get_short(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_SHORT ? &variant->value.as_short : NULL;
+}
+
+const int8_t*
+exess_get_byte(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_BYTE ? &variant->value.as_byte : NULL;
+}
+
+const uint64_t*
+exess_get_ulong(const ExessVariant* const variant)
+{
+ return (variant->datatype == EXESS_NON_NEGATIVE_INTEGER ||
+ variant->datatype == EXESS_ULONG ||
+ variant->datatype == EXESS_POSITIVE_INTEGER)
+ ? &variant->value.as_ulong
+ : NULL;
+}
+
+const uint32_t*
+exess_get_uint(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_UINT ? &variant->value.as_uint : NULL;
+}
+
+const uint16_t*
+exess_get_ushort(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_USHORT ? &variant->value.as_ushort : NULL;
+}
+
+const uint8_t*
+exess_get_ubyte(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_UBYTE ? &variant->value.as_ubyte : NULL;
+}
+
+const ExessBlob*
+exess_get_blob(const ExessVariant* const variant)
+{
+ return (variant->datatype == EXESS_HEX || variant->datatype == EXESS_BASE64)
+ ? &variant->value.as_blob
+ : NULL;
+}
+
+const ExessDuration*
+exess_get_duration(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_DURATION ? &variant->value.as_duration
+ : NULL;
+}
+
+const ExessDateTime*
+exess_get_datetime(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_DATETIME ? &variant->value.as_datetime
+ : NULL;
+}
+
+const ExessTime*
+exess_get_time(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_TIME ? &variant->value.as_time : NULL;
+}
+
+const ExessDate*
+exess_get_date(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_DATE ? &variant->value.as_date : NULL;
+}
+
+// Reading and Writing
+
+ExessResult
+exess_read_variant(ExessVariant* const out,
+ ExessDatatype datatype,
+ const char* const str)
+{
+ ExessResult r = {EXESS_UNSUPPORTED, 0};
+
+ out->datatype = datatype;
+
+ switch (datatype) {
+ case EXESS_NOTHING:
+ break;
+ case EXESS_BOOLEAN:
+ return exess_read_boolean(&out->value.as_bool, str);
+ case EXESS_DECIMAL:
+ return exess_read_decimal(&out->value.as_double, str);
+ case EXESS_DOUBLE:
+ return exess_read_double(&out->value.as_double, str);
+ case EXESS_FLOAT:
+ return exess_read_float(&out->value.as_float, str);
+ case EXESS_INTEGER:
+ return exess_read_long(&out->value.as_long, str);
+
+ case EXESS_NON_POSITIVE_INTEGER:
+ if (!(r = exess_read_long(&out->value.as_long, str)).status) {
+ if (out->value.as_long > 0) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+ }
+ break;
+
+ case EXESS_NEGATIVE_INTEGER:
+ if (!(r = exess_read_long(&out->value.as_long, str)).status) {
+ if (out->value.as_long >= 0) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+ }
+ break;
+
+ case EXESS_LONG:
+ return exess_read_long(&out->value.as_long, str);
+ case EXESS_INT:
+ return exess_read_int(&out->value.as_int, str);
+ case EXESS_SHORT:
+ return exess_read_short(&out->value.as_short, str);
+ case EXESS_BYTE:
+ return exess_read_byte(&out->value.as_byte, str);
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ return exess_read_ulong(&out->value.as_ulong, str);
+ case EXESS_UINT:
+ return exess_read_uint(&out->value.as_uint, str);
+ case EXESS_USHORT:
+ return exess_read_ushort(&out->value.as_ushort, str);
+ case EXESS_UBYTE:
+ return exess_read_ubyte(&out->value.as_ubyte, str);
+
+ case EXESS_POSITIVE_INTEGER:
+ if (!(r = exess_read_ulong(&out->value.as_ulong, str)).status) {
+ if (out->value.as_ulong <= 0) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+ }
+ break;
+
+ case EXESS_DURATION:
+ return exess_read_duration(&out->value.as_duration, str);
+ case EXESS_DATETIME:
+ return exess_read_datetime(&out->value.as_datetime, str);
+ case EXESS_TIME:
+ return exess_read_time(&out->value.as_time, str);
+ case EXESS_DATE:
+ return exess_read_date(&out->value.as_date, str);
+ case EXESS_HEX:
+ return exess_read_hex(&out->value.as_blob, str);
+ case EXESS_BASE64:
+ return exess_read_base64(&out->value.as_blob, str);
+ }
+
+ return r;
+}
+
+ExessResult
+exess_write_variant(const ExessVariant variant,
+ const size_t buf_size,
+ char* const buf)
+{
+ if (buf_size > 0) {
+ buf[0] = '\0';
+ }
+
+ switch (variant.datatype) {
+ case EXESS_NOTHING:
+ break;
+ case EXESS_BOOLEAN:
+ return exess_write_boolean(variant.value.as_bool, buf_size, buf);
+ case EXESS_DECIMAL:
+ return exess_write_decimal(variant.value.as_double, buf_size, buf);
+ case EXESS_DOUBLE:
+ return exess_write_double(variant.value.as_double, buf_size, buf);
+ case EXESS_FLOAT:
+ return exess_write_float(variant.value.as_float, buf_size, buf);
+ case EXESS_INTEGER:
+ case EXESS_NON_POSITIVE_INTEGER:
+ case EXESS_NEGATIVE_INTEGER:
+ case EXESS_LONG:
+ return exess_write_long(variant.value.as_long, buf_size, buf);
+ case EXESS_INT:
+ return exess_write_int(variant.value.as_int, buf_size, buf);
+ case EXESS_SHORT:
+ return exess_write_short(variant.value.as_short, buf_size, buf);
+ case EXESS_BYTE:
+ return exess_write_byte(variant.value.as_byte, buf_size, buf);
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ return exess_write_ulong(variant.value.as_ulong, buf_size, buf);
+ case EXESS_UINT:
+ return exess_write_uint(variant.value.as_uint, buf_size, buf);
+ case EXESS_USHORT:
+ return exess_write_ushort(variant.value.as_ushort, buf_size, buf);
+ case EXESS_UBYTE:
+ return exess_write_ubyte(variant.value.as_ubyte, buf_size, buf);
+ case EXESS_POSITIVE_INTEGER:
+ return exess_write_ulong(variant.value.as_ulong, buf_size, buf);
+ case EXESS_DURATION:
+ return exess_write_duration(variant.value.as_duration, buf_size, buf);
+ case EXESS_DATETIME:
+ return exess_write_datetime(variant.value.as_datetime, buf_size, buf);
+ case EXESS_TIME:
+ return exess_write_time(variant.value.as_time, buf_size, buf);
+ case EXESS_DATE:
+ return exess_write_date(variant.value.as_date, buf_size, buf);
+ case EXESS_HEX:
+ if (variant.value.as_blob.data) {
+ return exess_write_hex(variant.value.as_blob.size,
+ (void*)variant.value.as_blob.data,
+ buf_size,
+ buf);
+ }
+ break;
+ case EXESS_BASE64:
+ if (variant.value.as_blob.data) {
+ return exess_write_base64(variant.value.as_blob.size,
+ (void*)variant.value.as_blob.data,
+ buf_size,
+ buf);
+ }
+ break;
+ }
+
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+}
diff --git a/subprojects/exess/src/warnings.h b/subprojects/exess/src/warnings.h
new file mode 100644
index 00000000..2b1f6587
--- /dev/null
+++ b/subprojects/exess/src/warnings.h
@@ -0,0 +1,46 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_WARNINGS_H
+#define EXESS_WARNINGS_H
+
+#if defined(__clang__)
+
+# define EXESS_DISABLE_CONVERSION_WARNINGS \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Wconversion\"") \
+ _Pragma("clang diagnostic ignored \"-Wdouble-promotion\"")
+
+# define EXESS_RESTORE_WARNINGS _Pragma("clang diagnostic pop")
+
+#elif defined(__GNUC__)
+
+# define EXESS_DISABLE_CONVERSION_WARNINGS \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wconversion\"") \
+ _Pragma("GCC diagnostic ignored \"-Wfloat-conversion\"") \
+ _Pragma("GCC diagnostic ignored \"-Wdouble-promotion\"")
+
+# define EXESS_RESTORE_WARNINGS _Pragma("GCC diagnostic pop")
+
+#else
+
+# define EXESS_DISABLE_CONVERSION_WARNINGS
+# define EXESS_RESTORE_WARNINGS
+
+#endif
+
+#endif // EXESS_WARNINGS_H
diff --git a/subprojects/exess/src/write_utils.c b/subprojects/exess/src/write_utils.c
new file mode 100644
index 00000000..fdac0754
--- /dev/null
+++ b/subprojects/exess/src/write_utils.c
@@ -0,0 +1,50 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "write_utils.h"
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <string.h>
+
+size_t
+write_two_digit_number(const uint8_t value,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ if (buf_size >= i + 1) {
+ buf[i] = (char)((value >= 10) ? ('0' + value / 10) : '0');
+ buf[i + 1] = (char)('0' + (value % 10));
+ }
+
+ return 2;
+}
+
+ExessResult
+write_special(const size_t string_length,
+ const char* const string,
+ const size_t buf_size,
+ char* const buf)
+{
+ if (buf_size < string_length + 1) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ memcpy(buf, string, string_length + 1);
+ return result(EXESS_SUCCESS, string_length);
+}
diff --git a/subprojects/exess/src/write_utils.h b/subprojects/exess/src/write_utils.h
new file mode 100644
index 00000000..ea941743
--- /dev/null
+++ b/subprojects/exess/src/write_utils.h
@@ -0,0 +1,85 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_WRITE_UTILS_H
+#define EXESS_WRITE_UTILS_H
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static inline size_t
+write_char(const char c, size_t buf_size, char* const buf, const size_t i)
+{
+ if (buf && buf_size >= i + 1) {
+ buf[i] = c;
+ }
+
+ return 1;
+}
+
+static inline size_t
+write_string(const size_t len,
+ const char* str,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ if (buf && buf_size >= i + len + 1) {
+ memcpy(buf + i, str, len);
+ buf[i + len] = 0;
+ }
+
+ return len;
+}
+
+static inline ExessResult
+end_write(const ExessStatus status,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ ExessResult r = {status, status > EXESS_EXPECTED_END ? 0 : i};
+
+ if (buf) {
+ if (!status && i >= buf_size) {
+ r.status = EXESS_NO_SPACE;
+ r.count = 0;
+ }
+
+ if (r.count < buf_size) {
+ buf[r.count] = '\0';
+ }
+ }
+
+ return r;
+}
+
+ExessResult
+write_digits(uint64_t value, size_t buf_size, char* buf, size_t i);
+
+size_t
+write_two_digit_number(uint8_t value, size_t buf_size, char* buf, size_t i);
+
+ExessResult
+write_special(size_t string_length,
+ const char* string,
+ size_t buf_size,
+ char* buf);
+
+#endif // EXESS_WRITE_UTILS_H
diff --git a/subprojects/exess/src/year.c b/subprojects/exess/src/year.c
new file mode 100644
index 00000000..e268ff80
--- /dev/null
+++ b/subprojects/exess/src/year.c
@@ -0,0 +1,98 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "date_utils.h"
+#include "int_math.h"
+#include "macros.h"
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+read_year_number(int16_t* const out, const char* const str)
+{
+ *out = 0;
+
+ // Read leading sign if present
+ size_t i = 0;
+ int sign = 1;
+ if (str[i] == '-') {
+ sign = -1;
+ ++i;
+ }
+
+ // Read digits
+ uint64_t magnitude = 0;
+ ExessResult r = exess_read_ulong(&magnitude, str + i);
+ if (r.status > EXESS_EXPECTED_END) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+
+ if (sign > 0) {
+ if (magnitude > (uint16_t)INT16_MAX) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ *out = (int16_t)magnitude;
+ } else {
+ const uint16_t min_magnitude = (uint16_t)(-(INT16_MIN + 1)) + 1;
+ if (magnitude > min_magnitude) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ if (magnitude == min_magnitude) {
+ *out = INT16_MIN;
+ } else {
+ *out = (int16_t)-magnitude;
+ }
+ }
+
+ return result(r.count >= 4 ? EXESS_SUCCESS : EXESS_EXPECTED_DIGIT, i);
+}
+
+ExessResult
+write_year_number(const int16_t value, const size_t buf_size, char* const buf)
+{
+ const uint32_t abs_year = (uint32_t)abs(value);
+ const uint8_t n_digits = exess_num_digits(abs_year);
+ const bool is_negative = value < 0;
+
+ if (!buf) {
+ return result(EXESS_SUCCESS, is_negative + MAX(4u, n_digits));
+ }
+
+ // Write sign
+ size_t i = 0;
+ if (is_negative) {
+ i += write_char('-', buf_size, buf, i);
+ }
+
+ // Write leading zeros to ensure we have at least 4 year digits
+ for (size_t j = n_digits; j < 4; ++j) {
+ i += write_char('0', buf_size, buf, i);
+ }
+
+ const ExessResult yr = exess_write_uint(abs_year, buf_size - i, buf + i);
+
+ return end_write(yr.status, buf_size, buf, i + yr.count);
+}