From 1d8851cbf528aba34b3ca0b8ddb54b085d6ca9c4 Mon Sep 17 00:00:00 2001 From: Lephenixnoir Date: Sun, 7 Aug 2022 11:32:57 +0200 Subject: [PATCH] libnum: add experimental string representation Currently only has hand-picked tests and very rough code, but it's a start. In the future, I want to have better tests, more options like printf's %e/%f/%g, and more versatile methods. --- libnum/CMakeLists.txt | 18 ++++++--- libnum/include/num/num.h | 47 ++++++++++++++++++++-- libnum/src/str.cpp | 84 ++++++++++++++++++++++++++++++++++++++-- libnum/test/unit_str.cpp | 54 ++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 libnum/test/unit_str.cpp diff --git a/libnum/CMakeLists.txt b/libnum/CMakeLists.txt index 78b2600..a98d896 100644 --- a/libnum/CMakeLists.txt +++ b/libnum/CMakeLists.txt @@ -21,15 +21,23 @@ install(DIRECTORY include/ DESTINATION ${INCDIR}) # Unit tests #--- -set(UNIT_TESTS +set(UNIT_TESTS_SCALAR test/unit_scalar.cpp test/unit_static.cpp) -if(NOT CMAKE_CROSSCOMPILING) - add_executable(numtest ${UNIT_TESTS}) - target_link_libraries(numtest PUBLIC num) +set(UNIT_TESTS_STR + test/unit_str.cpp) - add_test(NAME "UnitTests" COMMAND numtest) +if(NOT CMAKE_CROSSCOMPILING) + add_executable(numtest_scalar ${UNIT_TESTS_SCALAR}) + target_link_libraries(numtest_scalar PUBLIC num) + add_test(NAME "UnitTestsScalar" COMMAND numtest_scalar) +endif() + +if(NOT CMAKE_CROSSCOMPILING) + add_executable(numtest_str ${UNIT_TESTS_STR}) + target_link_libraries(numtest_str PUBLIC num) + add_test(NAME "UnitTestsStr" COMMAND numtest_str) endif() #--- diff --git a/libnum/include/num/num.h b/libnum/include/num/num.h index f1d37e2..dac83b6 100644 --- a/libnum/include/num/num.h +++ b/libnum/include/num/num.h @@ -5,10 +5,37 @@ //---------------------------------------------------------------------------// // num.num: Fixed-point numerical types // -// This header provides numerical types of various fixed-point sizes. The base -// type num is num32, and other data structures outside of this header -// (vectors, matrices, etc.) default to it. Other types are useful for storage -// and sometimes intermediate computation steps. +// This header provides fixed-point numerical types with the following shapes: +// num8: 0: 8, unsigned (only values 0.x) +// num16: 8: 8, signed (from -128 to 127.x) +// num32: 16:16, signed (from -32768 to 32767.x) +// num64: 32:32, signed (from -2147483648 to 2147483647.x) +// +// The canonical type for computations is `num`, which is an alias for num32. +// +// Type safety is enforced by never allowing implicit casts whenever that would +// narrow the range. To convert a num64 to a num32 or an int to a num16, the +// constructor must be called explicitly. This means that overflows can only +// happen during computation and explicit conversion, which are easier to track +// down and check than implicit conversions. +// +// TODO: Currently all constructors are explicit, even eg. num8 -> num16 +// +// The API for each fixed-point type consists of: +// - Explicit constructors and cast operators to convert to and from integers, +// floating-point types and other fixed-point types. +// - Minimum and maximum values as int (minInt, maxInt) and double (minDouble, +// maxDouble). Note that num64 values don't fit in a double so the accuracy +// of the latter two isn't perfect. +// - Basic arithmetic (+, -, *, /, %, +=, -=, *=, /=, %=). +// - Comparisons with itself and with int (range-safe). +// - Conversions to strings: +// * strToBuffer(): appends the string representation of the value and a NUL +// byte to a char *; returns the number of characters (excluding the NUL). +// (TODO: experimental) +// * TODO: Other string conversion functions with more options +// - floor(), ceil() and frac() methods +// - TODO: More functions to do the equivalent of without floats //--- /* TODO: Conversion with float/double: use the binary format efficiently @@ -127,6 +154,9 @@ struct num8 /* Limits as double */ static constexpr double minDouble = 0.0; static constexpr double maxDouble = double(0xff) / 256; + + /* String representations */ + int strToBuffer(char *str); }; /* num16: Signed 8:8 fixed-point type @@ -231,6 +261,9 @@ struct num16 /* Limits as double */ static constexpr double minDouble = -128.0; static constexpr double maxDouble = double(0x7fff) / 256; + + /* String representations */ + int strToBuffer(char *str); }; /* num32: Signed 16:16 fixed-point type @@ -337,6 +370,9 @@ struct num32 /* Limits as double */ static constexpr double minDouble = -32768.0; static constexpr double maxDouble = double(0x7fffffff) / 65536; + + /* String representations */ + int strToBuffer(char *str); }; /* Arithmetic with integers */ @@ -434,6 +470,9 @@ struct num64 represent the entirety of the maximum value. */ static constexpr double minDouble = -2147483648.0; static constexpr double maxDouble = 2147483648.0 - double(1) / 2147483648; + + /* String representations */ + int strToBuffer(char *str); }; /* The following concept identifies the four num types */ diff --git a/libnum/src/str.cpp b/libnum/src/str.cpp index c0e9d87..9a283af 100644 --- a/libnum/src/str.cpp +++ b/libnum/src/str.cpp @@ -1,10 +1,88 @@ #include +#include using namespace libnum; /* Digits of the decimal part, from most to least significant. Returns the - number of digits (which is 0 when x=0) */ -static int decimal_digits(char *str, num64 x) + number of digits (which is 0 when x=0). The operand should be nonnegative. + The string should have room for 32 characters; no NUL is added. */ +template requires(is_num && N >= 0) +static int decimalDigitsBase(char *str, T x) { -// x = mod_64(x, num64_const(1)); + I v = x.frac().v; + int i = 0; + + while(v != 0) { + v *= 10; + I digit = v >> N; + str[i++] = (v >> N) + '0'; + v -= digit << N; + } + return i; +} + +template requires(is_num) +static int decimalDigits(char *str, T x) { return 0; } +template<> int decimalDigits(char *str, num8 x) { + return decimalDigitsBase(str, x); +} +template<> int decimalDigits(char *str, num16 x) { + return decimalDigitsBase(str, x); +} +template<> int decimalDigits(char *str, num32 x) { + return decimalDigitsBase(str, x); +} +template<> int decimalDigits(char *str, num64 x) { + return decimalDigitsBase(str, x); +} + +/* TODO: Complex string representations like %f/%e/%g + Generates the string representation of x in str; returns the number of + characters written. A NUL terminator is added (but not counted in the return + value). A total of 45 bytes is required for the longest 64-bit value. */ +template requires(is_num) +static int toString(char *str, T x) +{ + int n = 0; + /* We need to be able to represent the opposite of INT_MIN */ + int64_t integral_part = (int)x; + + if(x.v == 0) { + str[0] = '0'; + str[1] = 0; + return 1; + } + if(x.v < 0) { + *str = '-'; + n++; + /* This might overflow, which is why we separated the integral part */ + x = -x; + integral_part = -integral_part - (x.frac().v != 0); + } + + n += sprintf(str + n, "%ld", integral_part); + if(x.frac().v != 0) { + str[n++] = '.'; + n += decimalDigits(str + n, x); + } + str[n] = 0; + return n; +} + +int num8::strToBuffer(char *str) +{ + return toString(str, *this); +} +int num16::strToBuffer(char *str) +{ + return toString(str, *this); +} +int num32::strToBuffer(char *str) +{ + return toString(str, *this); +} +int num64::strToBuffer(char *str) +{ + return toString(str, *this); +} diff --git a/libnum/test/unit_str.cpp b/libnum/test/unit_str.cpp new file mode 100644 index 0000000..7e2cb1f --- /dev/null +++ b/libnum/test/unit_str.cpp @@ -0,0 +1,54 @@ +//---------------------------------------------------------------------------// +// ," /\ ", Azur: A game engine for CASIO fx-CG and PC // +// | _/__\_ | Designed by Lephe' and the Planète Casio community. // +// "._`\/'_." License: MIT // +//---------------------------------------------------------------------------// +// unit_str.cpp: Unit tests for string representations + +#include +#include +#include "unit_sample.h" +#include "unit_check.h" + +#define CHECK(EXPR, MSG) check(EXPR, #MSG) +#define CHECK_STR(value, exp) \ + CHECK(({ value.strToBuffer(_str); !strcmp(_str, exp); }), value: exp) + +bool runBasicTest(void) +{ + char _str[128]; + Checker c; + + c.CHECK_STR(num8(0), "0"); + c.CHECK_STR(num16(1), "1"); + c.CHECK_STR(num16(-1), "-1"); + c.CHECK_STR(num16(-0.25), "-0.25"); + c.CHECK_STR(num8(0.5), "0.5"); + c.CHECK_STR(num8(0.25), "0.25"); + c.CHECK_STR(num32(0.53125), "0.53125"); + c.CHECK_STR(num32(-73), "-73"); + c.CHECK_STR(num32(-5329), "-5329"); + c.CHECK_STR(num32(-32767), "-32767"); + c.CHECK_STR(num8(0.99609375), "0.99609375"); + c.CHECK_STR(num16(-127), "-127"); + c.CHECK_STR(num16(-30.6015625), "-30.6015625"); + c.CHECK_STR(num16(-128), "-128"); /* Overflow on -128 --> 128 after sign */ + + num64 x64; + x64.v = 1; + c.CHECK_STR(x64, "0.00000000023283064365386962890625"); + x64.v = -1; + c.CHECK_STR(x64, "-0.00000000023283064365386962890625"); + + return c.successful(); +} + +int main(void) +{ + bool success = true; + + printf("Testing a handful of pre-written inputs...\n"); + success &= runBasicTest(); + + return (success ? 0 : 1); +}