diff options
Diffstat (limited to 'subprojects/exess/src/decimal.c')
-rw-r--r-- | subprojects/exess/src/decimal.c | 267 |
1 files changed, 267 insertions, 0 deletions
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); +} |