commit fd010ec112466fc92f1db235c6987c760303a217 Author: SoraKatadzuma Date: Sat Sep 12 08:27:01 2026 -0500 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2744b01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.chinook/ +.environ/ +.vscode/ diff --git a/chinookfile b/chinookfile new file mode 100644 index 0000000..4f9e128 --- /dev/null +++ b/chinookfile @@ -0,0 +1,29 @@ +name: strview +gpid: malunal +semv: 1.0.0 + +requires: +- remote: git@git.erasit.com:malunal/microtest + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/types + branch: v1.0.0 + +targets: +- name: malunal.strview + type: archive + deps: + - malunal.types + srcs: + - ./sources/strview.c + +tests: +- name: malunal.strview.tests + type: program + deps: + - malunal.microtest + - malunal.strview + srcs: + - ./tests/strview.c + +exports: +- malunal.strview diff --git a/include/malunal/strview.h b/include/malunal/strview.h new file mode 100644 index 0000000..63e76af --- /dev/null +++ b/include/malunal/strview.h @@ -0,0 +1,188 @@ +/** + * @file strview.h + * @brief Contains the definition of the string view structure and functions. + * @author John Christman (sorakatadzuma@gmail.com) + * @copyright Malunal Studios, LLC. + */ +#include "malunal/type.h" + +#ifndef MALUNAL_STRVIEW_HEADER +#define MALUNAL_STRVIEW_HEADER + +/** + * @def STRVIEW_NPOS + * @brief Defines the null position of a string view. + * @details When received as a return result: indicates that the function could + * not operate on the strview provided, or indicates that the function + * would or did iterate beyond the bounds of the string view. + */ +#define STRVIEW_NPOS ((malunal_size_t)-1) + + +/** + * @struct strview + * @brief Defines a string view. + * @details A string view is a length based addressable pointer to an array of + * characters. Often, it is used in place of regular strings to remove + * the reliance on null-terminated strings and needing to store a copy + * of the null-terminated string, or its substring. + */ +define_struct(strview) { + /** + * @brief A pointer to the beginning of the string view. + * @details This points to the beginning of the string view, treating it like + * an array of characters that can be indexed. + */ + malunal_cstr_t beg; + + /** + * @brief A pointer to the ending of the string view. + * @details This points to the ending of the string view, treating it like an + * array of characters that can be indexed. + */ + malunal_cstr_t end; +}; + +/** + * @brief Creates a string view from the provided @c data and @c length. + * @param data A pointer to the data of the string view. + * @param length The length of the string view in characters. + * @returns The constructed string view. + */ +strview_t +strview( + malunal_cstr_t data, + malunal_size_t length +); + +/** + * @brief Creates a string view from the provided null-terminated @c data. + * @param data A pointer to the data of the string view. + * @returns The constructed string view. + */ +strview_t +strview_from_cstr( + malunal_cstr_t data +); + +/** + * @brief Provides the length of the string view. + * @param self A pointer to the string view to get the length of. + * @returns The length of the string view, zero if the pointer is null. + */ +malunal_size_t +strview_length( + strview_iptr_t self +); + +/** + * @brief Moves the start of the view forward by @c count. + * @param self The strview to trim by @c count. + * @param count The number of characters to trim by. + * @remarks This will mutate the provided string view directly. Provide a copy + * of the string view you wish to trim, if you want to preserve the + * original. + */ +malunal_void_t +strview_ltrim( + strview_mptr_t self, + malunal_size_t count +); + +/** + * @brief Moves the end of the view backward by @c count. + * @param self The strview to trim by @c count. + * @param count The number of characters to trim by. + * @remarks This will mutate the provided string view directly. Provide a copy + * of the string view you wish to trim, if you want to preserve the + * original. + */ +malunal_void_t +strview_rtrim( + strview_mptr_t self, + malunal_size_t count +); + +/** + * @brief Creates a subview of the current string view. + * @param self The string view to subview. + * @param start Where to subview from. + * @param count How many characters to subview. + * @returns The constructed subview. + * @remarks The result of this function can be a string view with null data and + * zero length if the input string view could not be subviewed. + */ +strview_t +strview_subview( + strview_iptr_t self, + malunal_size_t start, + malunal_size_t count +); + +/** + * @brief Provides the equality result of two string views. + * @param self The string view to compare against. + * @param other The string view to compare with. + * @retval true If the comparison of the two string views is zero. + * @retval false If the comparision of the two string views is non-zero. + */ +malunal_bool_t +strview_equals( + strview_iptr_t self, + strview_iptr_t other +); + +/** + * @brief Provides the comparison result of two string views. + * @param self The string view to compare against. + * @param other The string view to compare with. + * @retval -1 If @c self is comparatively less than @c other. + * @retval 0 If @c self is comparatively equal to @c other. + * @retval 1 if @c self is comparatively more than @c other. + */ +malunal_int32_t +strview_compare( + strview_iptr_t self, + strview_iptr_t other +); + +/** + * @brief Indicates if @c self starts with @c other. + * @param self The string view to compare against. + * @param other The string view to compare with. + * @retval true If @c self does start with @c other. + * @retval false If @c self does not start with @c other. + */ +malunal_bool_t +strview_starts_with( + strview_iptr_t self, + strview_iptr_t other +); + +/** + * @brief Indicates if @c self ends with @c other. + * @param self The string view to compare against. + * @param other The string view to compare with. + * @retval true If @c self does end with @c other. + * @retval false If @c self does not end with @c other. + */ +malunal_bool_t +strview_ends_with( + strview_iptr_t self, + strview_iptr_t other +); + +/** + * @brief Indicates if @c self contains @c other. + * @param self The string view to compare against. + * @param other The string view to compare with. + * @retval true If @c self does contain @c other. + * @retval false If @c self does not contain @c other. + */ +malunal_bool_t +strview_contains( + strview_iptr_t self, + strview_iptr_t other +); + +#endif /* MALUNAL_STRVIEW_HEADER */ diff --git a/sources/strview.c b/sources/strview.c new file mode 100644 index 0000000..9d9762c --- /dev/null +++ b/sources/strview.c @@ -0,0 +1,180 @@ +#include +#include "malunal/strview.h" + + +strview_t +strview( + malunal_cstr_t data, + malunal_size_t length +) { + return (strview_t) { + .beg = data, + .end = data + length + }; +} + +strview_t +strview_from_cstr( + malunal_cstr_t data +) { + return data != null + ? strview(data, strlen(data)) + : strview(null, 0); +} + +malunal_size_t +strview_length( + strview_iptr_t self +) { + return self != null + ? self->end - self->beg + : 0; +} + +malunal_void_t +strview_ltrim( + strview_mptr_t self, + malunal_size_t count +) { + malunal_size_t length = + strview_length(self); + if (length == 0) + return; + + if (length < count) + count = length; + self->beg += count; +} + +malunal_void_t +strview_rtrim( + strview_mptr_t self, + malunal_size_t count +) { + malunal_size_t length = + strview_length(self); + if (length == 0) + return; + + if (length < count) + count = length; + self->end -= count; +} + +strview_t +strview_subview( + strview_iptr_t self, + malunal_size_t start, + malunal_size_t count +) { + if (self == null) + return strview(null, 0); + + strview_t res = { self->beg, self->end }; + malunal_size_t len = strview_length(&res); + if (start > len) + return strview(null, 0); + + strview_ltrim(&res, start); + len = strview_length(&res); + if (count > len) + return strview(null, 0); + + strview_rtrim(&res, len - count); + return res; +} + +malunal_bool_t +strview_equals( + strview_iptr_t self, + strview_iptr_t other +) { + return strview_compare(self, other) == 0; +} + +malunal_int32_t +strview_compare( + strview_iptr_t self, + strview_iptr_t other +) { + if (self == null || other == null) { + if (self < other) return -1; + if (self > other) return 1; + if (self == other) return 0; + } + + malunal_size_t hlen = strview_length(self); + malunal_size_t nlen = strview_length(other); + if (hlen < nlen) return -1; + if (hlen > nlen) return 1; + + malunal_size_t smallest = hlen < nlen ? hlen : nlen; + return strncmp(self->beg, other->beg, smallest); +} + +malunal_bool_t +strview_starts_with( + strview_iptr_t self, + strview_iptr_t other +) { + if (self == null) return false; + if (other == null) return true; + + malunal_size_t hlen = strview_length(self); + malunal_size_t nlen = strview_length(other); + if (nlen == 0) + return true; + + return hlen >= nlen + ? strncmp(self->beg, other->beg, nlen) == 0 + : false; +} + +malunal_bool_t +strview_ends_with( + strview_iptr_t self, + strview_iptr_t other +) { + if (self == null) return false; + if (other == null) return true; + + malunal_size_t hlen = strview_length(self); + malunal_size_t nlen = strview_length(other); + if (nlen == 0) + return true; + + if (hlen < nlen) + return false; + + malunal_size_t diff = hlen - nlen; + malunal_cstr_t beg = self->beg + diff; + return strncmp(beg, other->beg, nlen) == 0; +} + +malunal_bool_t +strview_contains( + strview_iptr_t self, + strview_iptr_t other +) { + if (self == null) return false; + if (other == null) return true; + + malunal_size_t hlen = strview_length(self); + malunal_size_t nlen = strview_length(other); + if (nlen == 0) + return true; + + if (hlen < nlen) + return false; + + // Poor man's sliding window + malunal_cstr_t beg = self->beg; + while (hlen >= nlen) { + if (strncmp(beg, other->beg, nlen) == 0) + return true; + beg++; + hlen--; + } + + return false; +} diff --git a/tests/strview.c b/tests/strview.c new file mode 100644 index 0000000..9c3d81f --- /dev/null +++ b/tests/strview.c @@ -0,0 +1,378 @@ +#include "malunal/microtest.h" +#include "malunal/strview.h" + +/* ------------------------------------------------------------------------- + * Construction: strview() / strview_from_cstr() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_construct, from_data_and_length) { + malunal_cstr_t data = "hello world"; + strview_t view = strview(data, 5); + + MICROTEST_EXPECT_EQ(view.beg, data); + MICROTEST_EXPECT_EQ(view.end, data + 5); + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)5); +} + +MICROTEST(strview_construct, zero_length) { + malunal_cstr_t data = "hello"; + strview_t view = strview(data, 0); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)0); + MICROTEST_EXPECT_EQ(view.beg, view.end); +} + +MICROTEST(strview_construct, from_cstr_matches_strlen) { + malunal_cstr_t data = "hello world"; + strview_t view = strview_from_cstr(data); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)strlen(data)); + MICROTEST_EXPECT_TRUE(memcmp(view.beg, data, strview_length(&view)) == 0); +} + +MICROTEST(strview_construct, from_cstr_empty) { + strview_t view = strview_from_cstr(""); + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)0); +} + +MICROTEST(strview_construct, from_cstr_matches_manual_construction) { + malunal_cstr_t data = "matching"; + strview_t fromcstr = strview_from_cstr(data); + strview_t manual = strview(data, (malunal_size_t)strlen(data)); + + MICROTEST_EXPECT_TRUE(strview_equals(&fromcstr, &manual)); +} + +/* ------------------------------------------------------------------------- + * strview_length() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_length, reports_correct_length) { + strview_t view = strview_from_cstr("0123456789"); + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)10); +} + +MICROTEST(strview_length, null_self_returns_zero) { + MICROTEST_EXPECT_EQ(strview_length(null), (malunal_size_t)0); +} + +/* ------------------------------------------------------------------------- + * strview_ltrim() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_ltrim, moves_start_forward) { + strview_t view = strview_from_cstr("hello world"); + strview_ltrim(&view, 6); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)5); + MICROTEST_EXPECT_TRUE(memcmp(view.beg, "world", 5) == 0); +} + +MICROTEST(strview_ltrim, zero_count_is_noop) { + strview_t view = strview_from_cstr("unchanged"); + strview_t original = view; + + strview_ltrim(&view, 0); + + MICROTEST_EXPECT_TRUE(strview_equals(&view, &original)); +} + +MICROTEST(strview_ltrim, full_length_yields_empty_view) { + strview_t view = strview_from_cstr("hello"); + strview_ltrim(&view, strview_length(&view)); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)0); +} + +/* ------------------------------------------------------------------------- + * strview_rtrim() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_rtrim, moves_end_backward) { + strview_t view = strview_from_cstr("hello world"); + strview_rtrim(&view, 6); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)5); + MICROTEST_EXPECT_TRUE(memcmp(view.beg, "hello", 5) == 0); +} + +MICROTEST(strview_rtrim, zero_count_is_noop) { + strview_t view = strview_from_cstr("unchanged"); + strview_t original = view; + + strview_rtrim(&view, 0); + + MICROTEST_EXPECT_TRUE(strview_equals(&view, &original)); +} + +MICROTEST(strview_rtrim, full_length_yields_empty_view) { + strview_t view = strview_from_cstr("hello"); + strview_rtrim(&view, strview_length(&view)); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)0); +} + +MICROTEST(strview_rtrim, combined_with_ltrim_isolates_middle) { + strview_t view = strview_from_cstr(" trimmed "); + strview_ltrim(&view, 2); + strview_rtrim(&view, 2); + + MICROTEST_EXPECT_EQ(strview_length(&view), (malunal_size_t)7); + MICROTEST_EXPECT_TRUE(memcmp(view.beg, "trimmed", 7) == 0); +} + +/* ------------------------------------------------------------------------- + * strview_subview() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_subview, middle_slice) { + strview_t view = strview_from_cstr("hello world"); + strview_t sub = strview_subview(&view, 6, 5); + + MICROTEST_EXPECT_EQ(strview_length(&sub), (malunal_size_t)5); + MICROTEST_EXPECT_TRUE(memcmp(sub.beg, "world", 5) == 0); +} + +MICROTEST(strview_subview, from_start) { + strview_t view = strview_from_cstr("hello world"); + strview_t sub = strview_subview(&view, 0, 5); + + MICROTEST_EXPECT_TRUE(memcmp(sub.beg, "hello", 5) == 0); +} + +MICROTEST(strview_subview, zero_count_is_empty) { + strview_t view = strview_from_cstr("hello world"); + strview_t sub = strview_subview(&view, 3, 0); + + MICROTEST_EXPECT_EQ(strview_length(&sub), (malunal_size_t)0); +} + +MICROTEST(strview_subview, out_of_bounds_start_fails) { + /* Per the header remarks, an invalid subview yields null data and zero + * length rather than reading past the end of the source view. */ + strview_t view = strview_from_cstr("hello"); + strview_t sub = strview_subview(&view, 100, 1); + + MICROTEST_EXPECT_EQ(strview_length(&sub), (malunal_size_t)0); + MICROTEST_EXPECT_NULL(sub.beg); +} + +MICROTEST(strview_subview, count_exceeding_bounds_fails) { + strview_t view = strview_from_cstr("hello"); + strview_t sub = strview_subview(&view, 3, 10); + + MICROTEST_EXPECT_EQ(strview_length(&sub), (malunal_size_t)0); + MICROTEST_EXPECT_NULL(sub.beg); +} + +MICROTEST(strview_subview, null_self_fails) { + strview_t sub = strview_subview(null, 0, 1); + + MICROTEST_EXPECT_EQ(strview_length(&sub), (malunal_size_t)0); + MICROTEST_EXPECT_NULL(sub.beg); +} + +/* ------------------------------------------------------------------------- + * strview_equals() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_equals, identical_content_is_equal) { + strview_t a = strview_from_cstr("identical"); + strview_t b = strview_from_cstr("identical"); + + MICROTEST_EXPECT_TRUE(strview_equals(&a, &b)); +} + +MICROTEST(strview_equals, different_content_is_not_equal) { + strview_t a = strview_from_cstr("hello"); + strview_t b = strview_from_cstr("world"); + + MICROTEST_EXPECT_FALSE(strview_equals(&a, &b)); +} + +MICROTEST(strview_equals, different_lengths_are_not_equal) { + strview_t a = strview_from_cstr("hello"); + strview_t b = strview_from_cstr("hello!"); + + MICROTEST_EXPECT_FALSE(strview_equals(&a, &b)); +} + +MICROTEST(strview_equals, empty_views_are_equal) { + strview_t a = strview_from_cstr(""); + strview_t b = strview_from_cstr(""); + + MICROTEST_EXPECT_TRUE(strview_equals(&a, &b)); +} + +MICROTEST(strview_equals, is_reflexive) { + strview_t a = strview_from_cstr("self"); + + MICROTEST_EXPECT_TRUE(strview_equals(&a, &a)); +} + +/* ------------------------------------------------------------------------- + * strview_compare() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_compare, equal_views_compare_zero) { + strview_t a = strview_from_cstr("apple"); + strview_t b = strview_from_cstr("apple"); + + MICROTEST_EXPECT_EQ(strview_compare(&a, &b), 0); +} + +MICROTEST(strview_compare, lesser_view_compares_negative) { + strview_t a = strview_from_cstr("apple"); + strview_t b = strview_from_cstr("banana"); + + MICROTEST_EXPECT_LT(strview_compare(&a, &b), 0); +} + +MICROTEST(strview_compare, greater_view_compares_positive) { + strview_t a = strview_from_cstr("banana"); + strview_t b = strview_from_cstr("apple"); + + MICROTEST_EXPECT_GT(strview_compare(&a, &b), 0); +} + +MICROTEST(strview_compare, shorter_prefix_compares_negative) { + strview_t a = strview_from_cstr("app"); + strview_t b = strview_from_cstr("apple"); + + MICROTEST_EXPECT_LT(strview_compare(&a, &b), 0); +} + +/* ------------------------------------------------------------------------- + * strview_starts_with() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_starts_with, true_when_prefix_matches) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_TRUE(strview_starts_with(&self, &other)); +} + +MICROTEST(strview_starts_with, false_when_prefix_differs) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("world"); + + MICROTEST_EXPECT_FALSE(strview_starts_with(&self, &other)); +} + +MICROTEST(strview_starts_with, false_when_other_longer_than_self) { + strview_t self = strview_from_cstr("hi"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_FALSE(strview_starts_with(&self, &other)); +} + +MICROTEST(strview_starts_with, true_for_empty_other) { + strview_t self = strview_from_cstr("hello"); + strview_t other = strview_from_cstr(""); + + MICROTEST_EXPECT_TRUE(strview_starts_with(&self, &other)); +} + +MICROTEST(strview_starts_with, true_when_equal) { + strview_t self = strview_from_cstr("hello"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_TRUE(strview_starts_with(&self, &other)); +} + +/* ------------------------------------------------------------------------- + * strview_ends_with() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_ends_with, true_when_suffix_matches) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("world"); + + MICROTEST_EXPECT_TRUE(strview_ends_with(&self, &other)); +} + +MICROTEST(strview_ends_with, false_when_suffix_differs) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_FALSE(strview_ends_with(&self, &other)); +} + +MICROTEST(strview_ends_with, false_when_other_longer_than_self) { + strview_t self = strview_from_cstr("hi"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_FALSE(strview_ends_with(&self, &other)); +} + +MICROTEST(strview_ends_with, true_for_empty_other) { + strview_t self = strview_from_cstr("hello"); + strview_t other = strview_from_cstr(""); + + MICROTEST_EXPECT_TRUE(strview_ends_with(&self, &other)); +} + +MICROTEST(strview_ends_with, true_when_equal) { + strview_t self = strview_from_cstr("hello"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_TRUE(strview_ends_with(&self, &other)); +} + +/* ------------------------------------------------------------------------- + * strview_contains() + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_contains, true_when_substring_present) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("lo wo"); + + MICROTEST_EXPECT_TRUE(strview_contains(&self, &other)); +} + +MICROTEST(strview_contains, false_when_substring_absent) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("xyz"); + + MICROTEST_EXPECT_FALSE(strview_contains(&self, &other)); +} + +MICROTEST(strview_contains, true_for_prefix) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_TRUE(strview_contains(&self, &other)); +} + +MICROTEST(strview_contains, true_for_suffix) { + strview_t self = strview_from_cstr("hello world"); + strview_t other = strview_from_cstr("world"); + + MICROTEST_EXPECT_TRUE(strview_contains(&self, &other)); +} + +MICROTEST(strview_contains, true_for_empty_other) { + strview_t self = strview_from_cstr("hello"); + strview_t other = strview_from_cstr(""); + + MICROTEST_EXPECT_TRUE(strview_contains(&self, &other)); +} + +MICROTEST(strview_contains, false_when_other_longer_than_self) { + strview_t self = strview_from_cstr("hi"); + strview_t other = strview_from_cstr("hello"); + + MICROTEST_EXPECT_FALSE(strview_contains(&self, &other)); +} + +/* ------------------------------------------------------------------------- + * STRVIEW_NPOS + * ---------------------------------------------------------------------- */ + +MICROTEST(strview_npos, is_max_size_value) { + MICROTEST_EXPECT_EQ(STRVIEW_NPOS, (malunal_size_t)-1); +} + +MICROTEST_MAIN() +