From 838861aeea80678d857504db08986e977d26b16c Mon Sep 17 00:00:00 2001 From: SoraKatadzuma Date: Sat, 12 Sep 2026 10:08:59 -0500 Subject: [PATCH] Conversion functions --- include/malunal/strview.h | 48 +++++++++- sources/strview.c | 135 ++++++++++++++++++++++++++ tests/strview.c | 196 +++++++++++++++++++++++++++++++++++++- 3 files changed, 377 insertions(+), 2 deletions(-) diff --git a/include/malunal/strview.h b/include/malunal/strview.h index 63e76af..67ef607 100644 --- a/include/malunal/strview.h +++ b/include/malunal/strview.h @@ -4,7 +4,7 @@ * @author John Christman (sorakatadzuma@gmail.com) * @copyright Malunal Studios, LLC. */ -#include "malunal/type.h" +#include "malunal/types/error.h" #ifndef MALUNAL_STRVIEW_HEADER #define MALUNAL_STRVIEW_HEADER @@ -18,6 +18,14 @@ */ #define STRVIEW_NPOS ((malunal_size_t)-1) +/** + * @brief Imports the @c strview_t error domain for error checking. + * @details The @c strview_t error domain is specific for string views to + * indicate an issue with functions relating to string views. + */ +extern +const error_domain_t +ERROR_DOMAIN_STRVIEW_T; /** * @struct strview @@ -43,6 +51,20 @@ define_struct(strview) { malunal_cstr_t end; }; + +/** + * @brief Defines the set of errors that may be triggered by a string view. + * @details This are used in conjunction with a domain to indicate an error + * that comes specifically from a string view or its functions and + * what that error means. + */ +typedef enum { + STRVIEW_ERROR_NULL_INPUT, + STRVIEW_ERROR_OUT_OF_RANGE, + STRVIEW_ERROR_UNCONSUMED, +} strview_error_t; + + /** * @brief Creates a string view from the provided @c data and @c length. * @param data A pointer to the data of the string view. @@ -185,4 +207,28 @@ strview_contains( strview_iptr_t other ); +/** + * @brief Converts the string view to an @c long @c long, ala @c strtoll. + * @param self The string view to convert. + * @param out If not null, populated with the converted value. + * @returns An error if the conversion could not be performed. + */ +error_t +strview_to_int64( + strview_iptr_t self, + malunal_int64_t* out +); + +/** + * @brief Converts the string view to a @c float, ala @c strtod. + * @param self The string view to convert. + * @param out If not null, populated with the converted value. + * @returns An error if the conversion could not be performed. + */ +error_t +strview_to_double( + strview_iptr_t self, + malunal_double_t* out +); + #endif /* MALUNAL_STRVIEW_HEADER */ diff --git a/sources/strview.c b/sources/strview.c index 9d9762c..0065ce2 100644 --- a/sources/strview.c +++ b/sources/strview.c @@ -1,6 +1,47 @@ +#include +#include #include #include "malunal/strview.h" +#define STRVIEW_NUMBUF_SIZE 64 + +static +malunal_cstr_t +describe(malunal_int32_t code) { + switch (code) { + case STRVIEW_ERROR_NULL_INPUT: + return "String view input was null."; + case STRVIEW_ERROR_OUT_OF_RANGE: + return "Conversion of string view to value was out of range."; + case STRVIEW_ERROR_UNCONSUMED: + return "String view was not consumed fully during conversion."; + } + + return "Unknown strview error"; +} + +const error_domain_t ERROR_DOMAIN_STRVIEW_T = { + .describe = &describe, + .name = "malunal.strview.error" +}; + +static malunal_size_t +strview__copy_to_buffer( + strview_iptr_t self, + malunal_char_t buffer[STRVIEW_NUMBUF_SIZE] +) { + malunal_size_t length = strview_length(self); + malunal_size_t copied = + length >= STRVIEW_NUMBUF_SIZE - 1 + ? STRVIEW_NUMBUF_SIZE - 1 + : length; + + if (copied > 0) + memcpy(buffer, self->beg, copied); + buffer[copied] = '\0'; + return copied; +} + strview_t strview( @@ -178,3 +219,97 @@ strview_contains( return false; } + +error_t +strview_to_int64( + strview_iptr_t self, + malunal_int64_t* out +) { + if (self == null) { + if (out != null) + *out = 0; + return (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_NULL_INPUT + }; + } + + malunal_char_t buffer[STRVIEW_NUMBUF_SIZE]; + malunal_size_t copied = strview__copy_to_buffer(self, buffer); + if (copied == 0) { + if (out != null) + *out = 0; + return (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_UNCONSUMED + }; + } + + malunal_str_t endptr; + malunal_int64_t value; + + errno = 0; + value = strtoll(buffer, &endptr, 10); + if (out != null) + *out = value; + + if (errno == ERANGE) + return (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_OUT_OF_RANGE + }; + + return endptr != buffer + copied + ? (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_UNCONSUMED + } + : NO_ERROR; +} + +error_t +strview_to_double( + strview_iptr_t self, + malunal_double_t* out +) { + if (self == null) { + if (out != null) + *out = 0.0; + return (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_NULL_INPUT + }; + } + + malunal_char_t buffer[STRVIEW_NUMBUF_SIZE]; + malunal_size_t copied = strview__copy_to_buffer(self, buffer); + if (copied == 0) { + if (out != null) + *out = 0.0; + return (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_UNCONSUMED + }; + } + + malunal_str_t endptr; + malunal_double_t value; + + errno = 0; + value = strtod(buffer, &endptr); + if (out != null) + *out = value; + + if (errno == ERANGE) + return (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_OUT_OF_RANGE + }; + + return endptr != buffer + copied + ? (error_t) { + .domain = &ERROR_DOMAIN_STRVIEW_T, + .code = STRVIEW_ERROR_UNCONSUMED + } + : NO_ERROR; +} diff --git a/tests/strview.c b/tests/strview.c index 9c3d81f..af10403 100644 --- a/tests/strview.c +++ b/tests/strview.c @@ -366,6 +366,201 @@ MICROTEST(strview_contains, false_when_other_longer_than_self) { MICROTEST_EXPECT_FALSE(strview_contains(&self, &other)); } +/* ------------------------------------------------------------------------- + * strview_to_int64() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_to_int64, positive_integer_succeeds) { + strview_t view = strview_from_cstr("12345"); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(out, (malunal_int64_t)12345); +} + +MICROTEST(strview_to_int64, negative_integer_succeeds) { + strview_t view = strview_from_cstr("-42"); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(out, (malunal_int64_t)-42); +} + +MICROTEST(strview_to_int64, zero_succeeds) { + strview_t view = strview_from_cstr("0"); + malunal_int64_t out = -1; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(out, (malunal_int64_t)0); +} + +MICROTEST(strview_to_int64, succeeds_on_subview_not_ending_at_buffer_end) { + malunal_cstr_t data = "12345 and then some trailing text"; + strview_t view = strview(data, 5); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(out, (malunal_int64_t)12345); +} + +MICROTEST(strview_to_int64, trailing_garbage_reports_unconsumed) { + strview_t view = strview_from_cstr("123abc"); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_UNCONSUMED); +} + +MICROTEST(strview_to_int64, non_numeric_reports_unconsumed) { + strview_t view = strview_from_cstr("abc"); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_UNCONSUMED); +} + +MICROTEST(strview_to_int64, empty_view_reports_unconsumed) { + strview_t view = strview_from_cstr(""); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_UNCONSUMED); +} + +MICROTEST(strview_to_int64, null_self_reports_null_input) { + malunal_int64_t out = 99; + error_t err = strview_to_int64(null, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_NULL_INPUT); + MICROTEST_EXPECT_EQ(out, (malunal_int64_t)0); +} + +MICROTEST(strview_to_int64, overflow_reports_out_of_range) { + strview_t view = strview_from_cstr("99999999999999999999999"); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_OUT_OF_RANGE); +} + +MICROTEST(strview_to_int64, underflow_reports_out_of_range) { + strview_t view = strview_from_cstr("-99999999999999999999999"); + malunal_int64_t out = 0; + error_t err = strview_to_int64(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_OUT_OF_RANGE); +} + +MICROTEST(strview_to_int64, null_out_pointer_does_not_crash) { + strview_t view = strview_from_cstr("12345"); + error_t err = strview_to_int64(&view, null); + + MICROTEST_EXPECT_NULL(err.domain); +} + +/* ------------------------------------------------------------------------- + * strview_to_double() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_to_double, positive_decimal_succeeds) { + strview_t view = strview_from_cstr("3.14"); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(out > 3.139 && out < 3.141); +} + +MICROTEST(strview_to_double, negative_decimal_succeeds) { + strview_t view = strview_from_cstr("-2.5"); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(out > -2.501 && out < -2.499); +} + +MICROTEST(strview_to_double, integer_like_value_succeeds) { + strview_t view = strview_from_cstr("42"); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(out > 41.999 && out < 42.001); +} + +MICROTEST(strview_to_double, succeeds_on_subview_not_ending_at_buffer_end) { + malunal_cstr_t data = "3.14 and then some trailing text"; + strview_t view = strview(data, 4); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_TRUE(out > 3.139 && out < 3.141); +} + +MICROTEST(strview_to_double, trailing_garbage_reports_unconsumed) { + strview_t view = strview_from_cstr("3.14xyz"); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_UNCONSUMED); +} + +MICROTEST(strview_to_double, non_numeric_reports_unconsumed) { + strview_t view = strview_from_cstr("abc"); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_UNCONSUMED); +} + +MICROTEST(strview_to_double, empty_view_reports_unconsumed) { + strview_t view = strview_from_cstr(""); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_UNCONSUMED); +} + +MICROTEST(strview_to_double, null_self_reports_null_input) { + malunal_double_t out = 99.0; + error_t err = strview_to_double(null, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_NULL_INPUT); + MICROTEST_EXPECT_TRUE(out == 0.0); +} + +MICROTEST(strview_to_double, overflow_reports_out_of_range) { + strview_t view = strview_from_cstr("1e400"); + malunal_double_t out = 0.0; + error_t err = strview_to_double(&view, &out); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)STRVIEW_ERROR_OUT_OF_RANGE); +} + +MICROTEST(strview_to_double, null_out_pointer_does_not_crash) { + strview_t view = strview_from_cstr("3.14"); + error_t err = strview_to_double(&view, null); + + MICROTEST_EXPECT_NULL(err.domain); +} + /* ------------------------------------------------------------------------- * STRVIEW_NPOS * ---------------------------------------------------------------------- */ @@ -375,4 +570,3 @@ MICROTEST(strview_npos, is_max_size_value) { } MICROTEST_MAIN() -