Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions code/logic/cstring.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,102 @@ cstring fossil_io_cstring_copy(ccstring str) {
return fossil_io_cstring_create(str);
}

static const char *units[] = {
"zero", "one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten", "eleven",
"twelve", "thirteen", "fourteen", "fifteen",
"sixteen", "seventeen", "eighteen", "nineteen"
};

static const char *tens[] = {
"", "", "twenty", "thirty", "forty", "fifty",
"sixty", "seventy", "eighty", "ninety"
};

// ---------------- Number -> Words ----------------
int fossil_io_cstring_number_to_words(int num, char *buffer, size_t size) {
if (!buffer || size == 0) return -1;
buffer[0] = '\0';

if (num < 0 || num > 9999) return -1; // Limit to 0..9999

if (num >= 1000) {
int thousands = num / 1000;
if (strlen(buffer) + strlen(units[thousands]) + 10 >= size) return -1;
strcat(buffer, units[thousands]);
strcat(buffer, " thousand");
num %= 1000;
if (num > 0) strcat(buffer, " ");
}

if (num >= 100) {
int hundreds = num / 100;
if (strlen(buffer) + strlen(units[hundreds]) + 10 >= size) return -1;
strcat(buffer, units[hundreds]);
strcat(buffer, " hundred");
num %= 100;
if (num > 0) strcat(buffer, " and ");
}

if (num >= 20) {
int t = num / 10;
if (strlen(buffer) + strlen(tens[t]) + 2 >= size) return -1;
strcat(buffer, tens[t]);
num %= 10;
if (num > 0) {
strcat(buffer, "-");
strcat(buffer, units[num]);
}
} else if (num > 0 || strlen(buffer) == 0) {
if (strlen(buffer) + strlen(units[num]) + 1 >= size) return -1;
strcat(buffer, units[num]);
}

return 0;
}

// ---------------- Words -> Number ----------------
static int fossil_io_word_to_value(const char *word) {
for (int i = 0; i < 20; i++) if (strcmp(word, units[i]) == 0) return i;
for (int i = 2; i < 10; i++) if (strcmp(word, tens[i]) == 0) return i * 10;
if (strcmp(word, "hundred") == 0) return -100; // multiplier
if (strcmp(word, "thousand") == 0) return -1000; // multiplier
return -1; // not found
}

int fossil_io_cstring_number_from_words(const char *str, int *out) {
if (!str || !out) return -1;

int total = 0;
int current = 0;

char buffer[256];
strncpy(buffer, str, sizeof(buffer)-1);
buffer[sizeof(buffer)-1] = '\0';

// lowercase and remove extra characters
for (char *p = buffer; *p; ++p) *p = (char)tolower(*p);

char *token = strtok(buffer, " -");
while (token) {
int val = fossil_io_word_to_value(token);
if (val >= 0) {
current += val;
} else if (val == -100) { // hundred
current *= 100;
} else if (val == -1000) { // thousand
total += current * 1000;
current = 0;
} else {
return -1; // unknown word
}
token = strtok(NULL, " -");
}

*out = total + current;
return 0;
}

cstring fossil_io_cstring_dup(ccstring str) {
if (!str) return NULL;
size_t length = strlen(str);
Expand Down
53 changes: 53 additions & 0 deletions code/logic/fossil/io/cstring.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,29 @@ void fossil_io_cstring_free(cstring str);
*/
cstring fossil_io_cstring_copy(ccstring str);

/**
* @brief Converts an English number string into an integer.
*
* Example: "twenty-three" -> 23
*
* @param str Input string containing the number in English.
* @param out Pointer to integer where the parsed number will be stored.
* @return 0 on success, non-zero on error (invalid input).
*/
int fossil_io_cstring_number_from_words(const char *str, int *out);

/**
* @brief Converts an integer number into its English representation.
*
* Example: 23 -> "twenty-three"
*
* @param num The integer to convert.
* @param buffer The output buffer to store the English string.
* @param size The size of the output buffer.
* @return 0 on success, non-zero if the buffer is too small.
*/
int fossil_io_cstring_number_to_words(int num, char *buffer, size_t size);

/**
* @brief Duplicates the given cstring.
*
Expand Down Expand Up @@ -437,6 +460,36 @@ namespace fossil {
fossil_io_cstring_free(_str);
}

/**
* Converts an English number string into an integer.
*
* Example: "twenty-three" -> 23
*
* @param str Input string containing the number in English.
* @return The parsed integer value, or throws std::invalid_argument on error.
*/
static int number_from_words(const std::string &str) {
int value = 0;
int result = fossil_io_cstring_number_from_words(str.c_str(), &value);
if (result != 0) throw std::invalid_argument("Invalid English number string");
return value;
}

/**
* Converts an integer number into its English representation.
*
* Example: 23 -> "twenty-three"
*
* @param num The integer to convert.
* @return The English representation as a std::string.
*/
static std::string number_to_words(int num) {
char buffer[128];
int result = fossil_io_cstring_number_to_words(num, buffer, sizeof(buffer));
if (result != 0) throw std::runtime_error("Buffer too small for English number string");
return std::string(buffer);
}

/**
* Creates a copy of the given cstring.
*
Expand Down
34 changes: 34 additions & 0 deletions code/tests/cases/test_cstring.c
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,37 @@ FOSSIL_TEST(c_test_cstring_append) {
fossil_io_cstring_free(str);
}

// Test fossil_io_cstring_number_from_words
FOSSIL_TEST(c_test_cstring_number_from_words) {
int value = 0;
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("twenty-three", &value));
ASSUME_ITS_EQUAL_I32(23, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one hundred", &value));
ASSUME_ITS_EQUAL_I32(100, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("zero", &value));
ASSUME_ITS_EQUAL_I32(0, value);

ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("not-a-number", &value) != 0);
}

// Test fossil_io_cstring_number_to_words
FOSSIL_TEST(c_test_cstring_number_to_words) {
char buffer[64];
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(23, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("twenty-three", buffer);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(100, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("one hundred", buffer);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(0, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("zero", buffer);

// Buffer too small
ASSUME_ITS_TRUE(fossil_io_cstring_number_to_words(123456789, buffer, 5) != 0);
}

// * * * * * * * * * * * * * * * * * * * * * * * *
// * Fossil Logic Test Pool
// * * * * * * * * * * * * * * * * * * * * * * * *
Expand Down Expand Up @@ -393,6 +424,9 @@ FOSSIL_TEST_GROUP(c_string_tests) {
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_strip_quotes);
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_append);

FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_number_from_words);
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_number_to_words);

FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_create_and_free);
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_write_and_read);
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_multiple_writes);
Expand Down
92 changes: 92 additions & 0 deletions code/tests/cases/test_cstring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,90 @@ FOSSIL_TEST(cpp_test_cstring_class_append) {
ASSUME_ITS_EQUAL_CSTR("Hello, World!", str.str());
}

FOSSIL_TEST(cpp_test_cstring_number_from_words_basic) {
int value = 0;
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("zero", &value));
ASSUME_ITS_EQUAL_I32(0, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one", &value));
ASSUME_ITS_EQUAL_I32(1, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("ten", &value));
ASSUME_ITS_EQUAL_I32(10, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("twenty-three", &value));
ASSUME_ITS_EQUAL_I32(23, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one hundred", &value));
ASSUME_ITS_EQUAL_I32(100, value);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one thousand two hundred thirty-four", &value));
ASSUME_ITS_EQUAL_I32(1234, value);
}

FOSSIL_TEST(cpp_test_cstring_number_from_words_invalid) {
int value = 0;
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("not-a-number", &value) != 0);
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("", &value) != 0);
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("twenty one hundred apples", &value) != 0);
}

FOSSIL_TEST(cpp_test_cstring_number_to_words_basic) {
char buffer[128];
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(0, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("zero", buffer);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(1, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("one", buffer);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(23, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("twenty-three", buffer);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(100, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("one hundred", buffer);

ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(1234, buffer, sizeof(buffer)));
ASSUME_ITS_EQUAL_CSTR("one thousand two hundred thirty-four", buffer);
}

FOSSIL_TEST(cpp_test_cstring_number_to_words_buffer_too_small) {
char buffer[8];
ASSUME_ITS_TRUE(fossil_io_cstring_number_to_words(123456, buffer, sizeof(buffer)) != 0);
}

FOSSIL_TEST(cpp_test_cstring_class_number_from_words_and_to_words) {
using fossil::io::CString;
ASSUME_ITS_EQUAL_I32(23, CString::number_from_words("twenty-three"));
ASSUME_ITS_EQUAL_I32(100, CString::number_from_words("one hundred"));
ASSUME_ITS_EQUAL_I32(0, CString::number_from_words("zero"));

ASSUME_ITS_EQUAL_CSTR("twenty-three", CString::number_to_words(23).c_str());
ASSUME_ITS_EQUAL_CSTR("one hundred", CString::number_to_words(100).c_str());
ASSUME_ITS_EQUAL_CSTR("zero", CString::number_to_words(0).c_str());
}

FOSSIL_TEST(cpp_test_cstring_class_number_from_words_exception) {
using fossil::io::CString;
bool thrown = false;
try {
CString::number_from_words("not-a-number");
} catch (const std::invalid_argument&) {
thrown = true;
}
ASSUME_ITS_TRUE(thrown);
}

FOSSIL_TEST(cpp_test_cstring_class_number_to_words_exception) {
using fossil::io::CString;
bool thrown = false;
try {
// Use a very large number that would overflow the buffer
CString::number_to_words(1234567890);
} catch (const std::runtime_error&) {
thrown = true;
}
ASSUME_ITS_TRUE(thrown);
}

// * * * * * * * * * * * * * * * * * * * * * * * *
// * Fossil Logic Test Pool
Expand Down Expand Up @@ -668,6 +752,14 @@ FOSSIL_TEST_GROUP(cpp_string_tests) {
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_strip_quotes);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_append);

FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_from_words_basic);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_from_words_invalid);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_to_words_basic);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_to_words_buffer_too_small);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_number_from_words_and_to_words);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_number_from_words_exception);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_number_to_words_exception);

FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_stream_class_create_and_free);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_stream_class_write_and_read);
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_stream_class_multiple_writes);
Expand Down
Loading