Repository: ashvardanian/StringZilla (language: rust)

Issue:
Bug: Last elements in `basic_charset` initialization are discarded
### Describe the bug

When initializing a `basic_charset` with character arrays such as those returned by `digits()` and others, the last element of the array is incorrectly discarded. 

https://github.com/ashvardanian/StringZilla/blob/152ed046701aceae2c3d4c995b0b661a9aa06d95/include/stringzilla/stringzilla.hpp#L192-L195

This is due to the use of `count_characters - 1` in the loop that populates the `basic_charset` bitset. 
https://github.com/ashvardanian/StringZilla/blob/152ed046701aceae2c3d4c995b0b661a9aa06d95/include/stringzilla/stringzilla.hpp#L283-L289

While this prevents including a null terminator for string literals like `sz::char_set("x")`, it causes incorrect behavior when handling character arrays that do not have a null terminator, resulting in the exclusion of the final character.

I believe this local block is the full extent of the affected code.

https://github.com/ashvardanian/StringZilla/blob/152ed046701aceae2c3d4c995b0b661a9aa06d95/include/stringzilla/stringzilla.hpp#L330-L341

### Steps to reproduce

```cpp
#include "stringzilla/stringzilla.hpp"

namespace sz = ashvardanian::stringzilla;

int main() {
        sz::string haystack = "239";

        // Test with null-terminated string
        assert(haystack.contains_only(sz::char_set("0123456789")));
        // Passes: null terminator is correctly discarded

        // Test with initializer list
        static std::initializer_list all = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
        assert (haystack.contains_only(sz::char_set {all}));
        // Passes: constructor for initializer list is called

        // Test with carray
        sz::carray<10> all_digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
        assert(haystack.contains_only(sz::char_set {all_digits}));
        assert(haystack.is_digit());
        // Fails: '9' is incorrectly discarded
}
```

### Expected behavior

No asserts

### StringZilla version

v3.11.0

### Operating System

Ubuntu 22.04.5

### Hardware architecture

x86

### Which interface are you using?

C++ bindings

### Contact Details

_No response_

### Are you open to being tagged as a contributor?

- [X] I am open to being mentioned in the project `.git` history as a contributor

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:
I will be able to write fix and add tests tomorrow

**Proposed Fix**

1. **Using `carray` with Null Terminator**

```cpp
inline carray<11> const &digits() noexcept { 
    static carray<11> const all = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\0'}; 
    return all;
}
```

2. **Using `std::initializer_list<char>`**

```cpp
inline auto const &digits() noexcept {
    static std::initializer_list all = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    return all;
}
```

Then the constructor for the initializer list will be called here:
https://github.com/ashvardanian/StringZilla/blob/152ed046701aceae2c3d4c995b0b661a9aa06d95/include/stringzilla/stringzilla.hpp#L335-L335


Thank you @alexbarev! Let's start with tests, and later merge a patch.

Fix patch:
diff --git a/include/stringzilla/stringzilla.hpp b/include/stringzilla/stringzilla.hpp
index a80da804..43869f08 100644
--- a/include/stringzilla/stringzilla.hpp
+++ b/include/stringzilla/stringzilla.hpp
@@ -283,7 +283,7 @@ class basic_charset {
     template <std::size_t count_characters>
     explicit basic_charset(char_type const (&chars)[count_characters]) noexcept : basic_charset() {
         static_assert(count_characters > 0, "Character array cannot be empty");
-        for (std::size_t i = 0; i < count_characters - 1; ++i) { // count_characters - 1 to exclude the null terminator
+        for (std::size_t i = 0; i != count_characters; ++i) {
             char_type c = chars[i];
             bitset_._u64s[sz_bitcast(sz_u8_t, c) >> 6] |= (1ull << (sz_bitcast(sz_u8_t, c) & 63u));
         }
@@ -292,7 +292,7 @@ class basic_charset {
     template <std::size_t count_characters>
     explicit basic_charset(std::array<char_type, count_characters> const &chars) noexcept : basic_charset() {
         static_assert(count_characters > 0, "Character array cannot be empty");
-        for (std::size_t i = 0; i < count_characters - 1; ++i) { // count_characters - 1 to exclude the null terminator
+        for (std::size_t i = 0; i != count_characters; ++i) {
             char_type c = chars[i];
             bitset_._u64s[sz_bitcast(sz_u8_t, c) >> 6] |= (1ull << (sz_bitcast(sz_u8_t, c) & 63u));
         }


Test patch:
diff --git a/scripts/test.cpp b/scripts/test.cpp
index eecc97f0..72379f78 100644
--- a/scripts/test.cpp
+++ b/scripts/test.cpp
@@ -137,6 +137,49 @@ static void test_arithmetical_utilities() {
                    (static_cast<sz_u8_t>(number) / static_cast<sz_u8_t>(divisor)));
 }
 
+/**
+ *  @brief  Tests various ASCII-based methods (e.g., `is_alpha`, `is_digit`)
+ *          provided by `sz::string` and `sz::string_view`.
+ */
+template <typename string_type>
+static void test_ascii_utilities() {
+
+    using str = string_type;
+
+    assert(!str("").is_alpha());
+    assert(str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ").is_alpha());
+    assert(!str("abc9").is_alpha());
+
+    assert(!str("").is_alnum());
+    assert(str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789").is_alnum());
+    assert(!str("abc!").is_alnum());
+
+    assert(str("").is_ascii());
+    assert(str("\x00x7F").is_ascii());
+    assert(!str("abc123🔥").is_ascii());
+
+    assert(!str("").is_digit());
+    assert(str("0123456789").is_digit());
+    assert(!str("012a").is_digit());
+
+    assert(!str("").is_lower());
+    assert(str("abcdefghijklmnopqrstuvwxyz").is_lower());
+    assert(!str("abcA").is_lower());
+    assert(!str("abc\n").is_lower());
+
+    assert(!str("").is_space());
+    assert(str(" \t\n\r\f\v").is_space());
+    assert(!str(" \t\r\na").is_space());
+
+    assert(!str("").is_upper());
+    assert(str("ABCDEFGHIJKLMNOPQRSTUVWXYZ").is_upper());
+    assert(!str("ABCa").is_upper());
+
+    assert(str("").is_printable());
+    assert(str("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+").is_printable());
+    assert(!str("012🔥").is_printable());
+}
+
 inline void expect_equality(char const *a, char const *b, std::size_t size) {
     if (std::memcmp(a, b, size) == 0) return;
     std::size_t mismatch_position = 0;
@@ -1583,6 +1626,8 @@ int main(int argc, char const **argv) {
 
     // Basic utilities
     test_arithmetical_utilities();
+    test_ascii_utilities<sz::string>();
+    test_ascii_utilities<sz::string_view>();
     test_memory_utilities();
     test_replacements();