diff options
| -rw-r--r-- | libfixmath/fix16.h | 11 | ||||
| -rw-r--r-- | libfixmath/fix16_str.c | 114 | ||||
| -rw-r--r-- | unittests/Makefile | 12 | ||||
| -rw-r--r-- | unittests/fix16_str_unittests.c | 117 |
4 files changed, 252 insertions, 2 deletions
diff --git a/libfixmath/fix16.h b/libfixmath/fix16.h index bab7254..3b41d2c 100644 --- a/libfixmath/fix16.h +++ b/libfixmath/fix16.h @@ -208,6 +208,17 @@ extern fix16_t fix16_log2(fix16_t x) FIXMATH_FUNC_ATTRS; */
extern fix16_t fix16_slog2(fix16_t x) FIXMATH_FUNC_ATTRS;
+/*! Convert fix16_t value to a string.
+ * Required buffer length for largest values is 13 bytes.
+ */
+extern void fix16_to_str(fix16_t value, char *buf, int decimals);
+
+/*! Convert string to a fix16_t value
+ * Ignores spaces at beginning and end. Returns fix16_overflow if
+ * value is too large or there were garbage characters.
+ */
+extern fix16_t fix16_from_str(const char *buf);
+
#ifdef __cplusplus
}
#include "fix16.hpp"
diff --git a/libfixmath/fix16_str.c b/libfixmath/fix16_str.c new file mode 100644 index 0000000..eff906f --- /dev/null +++ b/libfixmath/fix16_str.c @@ -0,0 +1,114 @@ +#include "fix16.h" +#include <stdbool.h> +#include <ctype.h> + +static const uint32_t scales[8] = { + /* 5 decimals is enough for full fix16_t precision */ + 1, 10, 100, 1000, 10000, 100000, 100000, 100000 +}; + +static char *itoa_loop(char *buf, uint32_t scale, uint32_t value, bool skip) +{ + while (scale) + { + unsigned digit = (value / scale); + + if (!skip || digit || scale == 1) + { + skip = false; + *buf++ = '0' + digit; + value %= scale; + } + + scale /= 10; + } + return buf; +} + +void fix16_to_str(fix16_t value, char *buf, int decimals) +{ + uint32_t uvalue = (value >= 0) ? value : -value; + if (value < 0) + *buf++ = '-'; + + /* Separate the integer and decimal parts of the value */ + unsigned intpart = uvalue >> 16; + uint32_t fracpart = uvalue & 0xFFFF; + uint32_t scale = scales[decimals & 7]; + fracpart = fix16_mul(fracpart, scale); + + if (fracpart >= scale) + { + /* Handle carry from decimal part */ + intpart++; + fracpart -= scale; + } + + /* Format integer part */ + buf = itoa_loop(buf, 10000, intpart, true); + + /* Format decimal part (if any) */ + if (scale != 1) + { + *buf++ = '.'; + buf = itoa_loop(buf, scale / 10, fracpart, false); + } + + *buf = '\0'; +} + +fix16_t fix16_from_str(const char *buf) +{ + while (isspace(*buf)) + buf++; + + /* Decode the sign */ + bool negative = (*buf == '-'); + if (*buf == '+' || *buf == '-') + buf++; + + /* Decode the integer part */ + uint32_t intpart = 0; + int count = 0; + while (isdigit(*buf)) + { + intpart *= 10; + intpart += *buf++ - '0'; + count++; + } + + if (count == 0 || count > 5 + || intpart > 32768 || (!negative && intpart > 32767)) + return fix16_overflow; + + fix16_t value = intpart << 16; + + /* Decode the decimal part */ + if (*buf == '.' || *buf == ',') + { + buf++; + + uint32_t fracpart = 0; + uint32_t scale = 1; + while (isdigit(*buf) && scale < 100000) + { + scale *= 10; + fracpart *= 10; + fracpart += *buf++ - '0'; + } + + value += fix16_div(fracpart, scale); + } + + /* Verify that there is no garbage left over */ + while (*buf != '\0') + { + if (!isdigit(*buf) && !isspace(*buf)) + return fix16_overflow; + + buf++; + } + + return negative ? -value : value; +} + diff --git a/unittests/Makefile b/unittests/Makefile index cb5dbb4..14dac10 100644 --- a/unittests/Makefile +++ b/unittests/Makefile @@ -5,10 +5,10 @@ CC = gcc CFLAGS = -g -O0 -I../libfixmath -Wall -Wextra -Werror # The files required for tests -FIX16_SRC = ../libfixmath/fix16.c ../libfixmath/fix16_sqrt.c \ +FIX16_SRC = ../libfixmath/fix16.c ../libfixmath/fix16_sqrt.c ../libfixmath/fix16_str.c \ ../libfixmath/fix16_exp.c ../libfixmath/fix16.h -all: run_fix16_unittests run_fix16_exp_unittests +all: run_fix16_unittests run_fix16_exp_unittests run_fix16_str_unittests clean: rm -f fix16_unittests_???? @@ -55,3 +55,11 @@ run_fix16_exp_unittests: fix16_exp_unittests fix16_exp_unittests: fix16_exp_unittests.c $(FIX16_SRC) $(CC) $(CFLAGS) $(DEFINES) -o $@ $^ -lm + +# Tests for string conversion, run only in default config +run_fix16_str_unittests: fix16_str_unittests + ./fix16_str_unittests > /dev/null + +fix16_str_unittests: fix16_str_unittests.c $(FIX16_SRC) + $(CC) $(CFLAGS) $(DEFINES) -o $@ $^ -lm + diff --git a/unittests/fix16_str_unittests.c b/unittests/fix16_str_unittests.c new file mode 100644 index 0000000..5a5bf94 --- /dev/null +++ b/unittests/fix16_str_unittests.c @@ -0,0 +1,117 @@ +#include <fix16.h> +#include <stdio.h> +#include <math.h> +#include <string.h> +#include <stdbool.h> +#include "unittests.h" + +int main() +{ + int status = 0; + + { + COMMENT("Testing fix16_to_str corner cases"); + char buf[13]; + + fix16_to_str(fix16_from_dbl(1234.5678), buf, 4); + printf("1234.5678 = %s\n", buf); + TEST(strcmp(buf, "1234.5678") == 0); + + fix16_to_str(fix16_from_dbl(-1234.5678), buf, 4); + printf("-1234.5678 = %s\n", buf); + TEST(strcmp(buf, "-1234.5678") == 0); + + fix16_to_str(0, buf, 0); + TEST(strcmp(buf, "0") == 0); + + fix16_to_str(fix16_from_dbl(0.9), buf, 0); + TEST(strcmp(buf, "1") == 0); + + fix16_to_str(1, buf, 5); + printf("(fix16_t)1 = %s\n", buf); + TEST(strcmp(buf, "0.00002") == 0); + + fix16_to_str(-1, buf, 5); + printf("(fix16_t)-1 = %s\n", buf); + TEST(strcmp(buf, "-0.00002") == 0); + + fix16_to_str(65535, buf, 5); + printf("(fix16_t)65535 = %s\n", buf); + TEST(strcmp(buf, "0.99998") == 0); + + fix16_to_str(65535, buf, 4); + printf("(fix16_t)65535 = %s\n", buf); + TEST(strcmp(buf, "1.0000") == 0); + + fix16_to_str(fix16_maximum, buf, 5); + printf("fix16_maximum = %s\n", buf); + TEST(strcmp(buf, "32767.99998") == 0); + + fix16_to_str(fix16_minimum, buf, 5); + printf("fix16_minimum = %s\n", buf); + TEST(strcmp(buf, "-32768.00000") == 0); + } + + { + COMMENT("Testing fix16_from_str corner cases"); + + TEST(fix16_from_str("1234.5678") == fix16_from_dbl(1234.5678)); + TEST(fix16_from_str("-1234.5678") == fix16_from_dbl(-1234.5678)); + TEST(fix16_from_str(" +1234,56780 ") == fix16_from_dbl(1234.5678)); + + TEST(fix16_from_str("0") == 0); + TEST(fix16_from_str("1") == fix16_one); + TEST(fix16_from_str("1.0") == fix16_one); + TEST(fix16_from_str("1.0000000000") == fix16_one); + + TEST(fix16_from_str("0.00002") == 1); + TEST(fix16_from_str("0.99998") == 65535); + + TEST(fix16_from_str("32767.99998") == fix16_maximum); + TEST(fix16_from_str("-32768.00000") == fix16_minimum); + } + + { + COMMENT("Extended testing for whole range"); + fix16_t value = fix16_minimum; + char testbuf[13]; + char goodbuf[13]; + + bool ok = true; + while (value < fix16_maximum) + { + double fvalue = fix16_to_dbl(value); + + /* Turns out we have to jump through some hoops to round + doubles perfectly for printing: + http://stackoverflow.com/questions/994764/rounding-doubles-5-sprintf + */ + fvalue = round(fvalue * 100000.)/100000.; + + snprintf(goodbuf, 13, "%0.5lf", fvalue); + fix16_to_str(value, testbuf, 5); + + if (strcmp(goodbuf, testbuf) != 0) + { + printf("Value (fix16_t)%d gave %s, should be %s\n", value, testbuf, goodbuf); + ok = false; + } + + fix16_t roundtrip = fix16_from_str(testbuf); + if (roundtrip != value) + { + printf("Roundtrip failed: (fix16_t)%d -> %s -> (fix16_t)%d\n", value, testbuf, roundtrip); + ok = false; + } + + value += 0x10001; + } + + TEST(ok); + } + + if (status != 0) + fprintf(stdout, "\n\nSome tests FAILED!\n"); + + return status; +} |
