Skip to content

Commit c4d7fd2

Browse files
Merge branch 'main' into silly_strings
2 parents b9436b4 + 9784b3d commit c4d7fd2

File tree

5 files changed

+281
-2
lines changed

5 files changed

+281
-2
lines changed

code/logic/cstring.c

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,102 @@ cstring fossil_io_cstring_copy(ccstring str) {
274274
return fossil_io_cstring_create(str);
275275
}
276276

277+
static const char *units[] = {
278+
"zero", "one", "two", "three", "four", "five",
279+
"six", "seven", "eight", "nine", "ten", "eleven",
280+
"twelve", "thirteen", "fourteen", "fifteen",
281+
"sixteen", "seventeen", "eighteen", "nineteen"
282+
};
283+
284+
static const char *tens[] = {
285+
"", "", "twenty", "thirty", "forty", "fifty",
286+
"sixty", "seventy", "eighty", "ninety"
287+
};
288+
289+
// ---------------- Number -> Words ----------------
290+
int fossil_io_cstring_number_to_words(int num, char *buffer, size_t size) {
291+
if (!buffer || size == 0) return -1;
292+
buffer[0] = '\0';
293+
294+
if (num < 0 || num > 9999) return -1; // Limit to 0..9999
295+
296+
if (num >= 1000) {
297+
int thousands = num / 1000;
298+
if (strlen(buffer) + strlen(units[thousands]) + 10 >= size) return -1;
299+
strcat(buffer, units[thousands]);
300+
strcat(buffer, " thousand");
301+
num %= 1000;
302+
if (num > 0) strcat(buffer, " ");
303+
}
304+
305+
if (num >= 100) {
306+
int hundreds = num / 100;
307+
if (strlen(buffer) + strlen(units[hundreds]) + 10 >= size) return -1;
308+
strcat(buffer, units[hundreds]);
309+
strcat(buffer, " hundred");
310+
num %= 100;
311+
if (num > 0) strcat(buffer, " and ");
312+
}
313+
314+
if (num >= 20) {
315+
int t = num / 10;
316+
if (strlen(buffer) + strlen(tens[t]) + 2 >= size) return -1;
317+
strcat(buffer, tens[t]);
318+
num %= 10;
319+
if (num > 0) {
320+
strcat(buffer, "-");
321+
strcat(buffer, units[num]);
322+
}
323+
} else if (num > 0 || strlen(buffer) == 0) {
324+
if (strlen(buffer) + strlen(units[num]) + 1 >= size) return -1;
325+
strcat(buffer, units[num]);
326+
}
327+
328+
return 0;
329+
}
330+
331+
// ---------------- Words -> Number ----------------
332+
static int fossil_io_word_to_value(const char *word) {
333+
for (int i = 0; i < 20; i++) if (strcmp(word, units[i]) == 0) return i;
334+
for (int i = 2; i < 10; i++) if (strcmp(word, tens[i]) == 0) return i * 10;
335+
if (strcmp(word, "hundred") == 0) return -100; // multiplier
336+
if (strcmp(word, "thousand") == 0) return -1000; // multiplier
337+
return -1; // not found
338+
}
339+
340+
int fossil_io_cstring_number_from_words(const char *str, int *out) {
341+
if (!str || !out) return -1;
342+
343+
int total = 0;
344+
int current = 0;
345+
346+
char buffer[256];
347+
strncpy(buffer, str, sizeof(buffer)-1);
348+
buffer[sizeof(buffer)-1] = '\0';
349+
350+
// lowercase and remove extra characters
351+
for (char *p = buffer; *p; ++p) *p = (char)tolower(*p);
352+
353+
char *token = strtok(buffer, " -");
354+
while (token) {
355+
int val = fossil_io_word_to_value(token);
356+
if (val >= 0) {
357+
current += val;
358+
} else if (val == -100) { // hundred
359+
current *= 100;
360+
} else if (val == -1000) { // thousand
361+
total += current * 1000;
362+
current = 0;
363+
} else {
364+
return -1; // unknown word
365+
}
366+
token = strtok(NULL, " -");
367+
}
368+
369+
*out = total + current;
370+
return 0;
371+
}
372+
277373
cstring fossil_io_cstring_dup(ccstring str) {
278374
if (!str) return NULL;
279375
size_t length = strlen(str);

code/logic/fossil/io/cstring.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,29 @@ char* fossil_io_cstring_zalgo(const char *str);
146146
*/
147147
cstring fossil_io_cstring_copy(ccstring str);
148148

149+
/**
150+
* @brief Converts an English number string into an integer.
151+
*
152+
* Example: "twenty-three" -> 23
153+
*
154+
* @param str Input string containing the number in English.
155+
* @param out Pointer to integer where the parsed number will be stored.
156+
* @return 0 on success, non-zero on error (invalid input).
157+
*/
158+
int fossil_io_cstring_number_from_words(const char *str, int *out);
159+
160+
/**
161+
* @brief Converts an integer number into its English representation.
162+
*
163+
* Example: 23 -> "twenty-three"
164+
*
165+
* @param num The integer to convert.
166+
* @param buffer The output buffer to store the English string.
167+
* @param size The size of the output buffer.
168+
* @return 0 on success, non-zero if the buffer is too small.
169+
*/
170+
int fossil_io_cstring_number_to_words(int num, char *buffer, size_t size);
171+
149172
/**
150173
* @brief Duplicates the given cstring.
151174
*
@@ -526,6 +549,36 @@ namespace fossil {
526549
fossil_io_cstring_free(_str);
527550
}
528551

552+
/**
553+
* Converts an English number string into an integer.
554+
*
555+
* Example: "twenty-three" -> 23
556+
*
557+
* @param str Input string containing the number in English.
558+
* @return The parsed integer value, or throws std::invalid_argument on error.
559+
*/
560+
static int number_from_words(const std::string &str) {
561+
int value = 0;
562+
int result = fossil_io_cstring_number_from_words(str.c_str(), &value);
563+
if (result != 0) throw std::invalid_argument("Invalid English number string");
564+
return value;
565+
}
566+
567+
/**
568+
* Converts an integer number into its English representation.
569+
*
570+
* Example: 23 -> "twenty-three"
571+
*
572+
* @param num The integer to convert.
573+
* @return The English representation as a std::string.
574+
*/
575+
static std::string number_to_words(int num) {
576+
char buffer[128];
577+
int result = fossil_io_cstring_number_to_words(num, buffer, sizeof(buffer));
578+
if (result != 0) throw std::runtime_error("Buffer too small for English number string");
579+
return std::string(buffer);
580+
}
581+
529582
/**
530583
* Creates a copy of the given cstring.
531584
*

code/meson.build

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1+
if host_machine.system() == 'darwin'
2+
add_languages('objc', native: false, required: true)
3+
add_languages('objcpp', native: false, required: true)
4+
endif
5+
16
subdir('logic')
2-
subdir('tests')
7+
subdir('tests')

code/tests/cases/test_cstring.c

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,37 @@ FOSSIL_TEST(c_test_cstring_zalgo_basic) {
492492
ASSUME_ITS_TRUE(result[0] == 'h');
493493
fossil_io_cstring_free(result);
494494
}
495+
496+
// Test fossil_io_cstring_number_from_words
497+
FOSSIL_TEST(c_test_cstring_number_from_words) {
498+
int value = 0;
499+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("twenty-three", &value));
500+
ASSUME_ITS_EQUAL_I32(23, value);
501+
502+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one hundred", &value));
503+
ASSUME_ITS_EQUAL_I32(100, value);
504+
505+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("zero", &value));
506+
ASSUME_ITS_EQUAL_I32(0, value);
507+
508+
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("not-a-number", &value) != 0);
509+
}
510+
511+
// Test fossil_io_cstring_number_to_words
512+
FOSSIL_TEST(c_test_cstring_number_to_words) {
513+
char buffer[64];
514+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(23, buffer, sizeof(buffer)));
515+
ASSUME_ITS_EQUAL_CSTR("twenty-three", buffer);
516+
517+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(100, buffer, sizeof(buffer)));
518+
ASSUME_ITS_EQUAL_CSTR("one hundred", buffer);
519+
520+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(0, buffer, sizeof(buffer)));
521+
ASSUME_ITS_EQUAL_CSTR("zero", buffer);
522+
523+
// Buffer too small
524+
ASSUME_ITS_TRUE(fossil_io_cstring_number_to_words(123456789, buffer, 5) != 0);
525+
}
495526

496527
// * * * * * * * * * * * * * * * * * * * * * * * *
497528
// * Fossil Logic Test Pool
@@ -530,7 +561,6 @@ FOSSIL_TEST_GROUP(c_string_tests) {
530561
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_normalize_spaces);
531562
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_strip_quotes);
532563
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_append);
533-
534564
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_silly_basic);
535565
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_silly_buffer_too_small);
536566
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_piglatin_vowel_start);
@@ -547,6 +577,8 @@ FOSSIL_TEST_GROUP(c_string_tests) {
547577
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_upper_snake_basic);
548578
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_upper_snake_with_symbols);
549579
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_zalgo_basic);
580+
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_number_from_words);
581+
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_number_to_words);
550582

551583
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_create_and_free);
552584
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_write_and_read);

code/tests/cases/test_cstring.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,91 @@ FOSSIL_TEST(cpp_test_cstring_class_zalgo) {
649649
ASSUME_ITS_TRUE(zalgo.length() >= str.length());
650650
}
651651

652+
FOSSIL_TEST(cpp_test_cstring_number_from_words_basic) {
653+
int value = 0;
654+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("zero", &value));
655+
ASSUME_ITS_EQUAL_I32(0, value);
656+
657+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one", &value));
658+
ASSUME_ITS_EQUAL_I32(1, value);
659+
660+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("ten", &value));
661+
ASSUME_ITS_EQUAL_I32(10, value);
662+
663+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("twenty-three", &value));
664+
ASSUME_ITS_EQUAL_I32(23, value);
665+
666+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one hundred", &value));
667+
ASSUME_ITS_EQUAL_I32(100, value);
668+
669+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_from_words("one thousand two hundred thirty-four", &value));
670+
ASSUME_ITS_EQUAL_I32(1234, value);
671+
}
672+
673+
FOSSIL_TEST(cpp_test_cstring_number_from_words_invalid) {
674+
int value = 0;
675+
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("not-a-number", &value) != 0);
676+
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("", &value) != 0);
677+
ASSUME_ITS_TRUE(fossil_io_cstring_number_from_words("twenty one hundred apples", &value) != 0);
678+
}
679+
680+
FOSSIL_TEST(cpp_test_cstring_number_to_words_basic) {
681+
char buffer[128];
682+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(0, buffer, sizeof(buffer)));
683+
ASSUME_ITS_EQUAL_CSTR("zero", buffer);
684+
685+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(1, buffer, sizeof(buffer)));
686+
ASSUME_ITS_EQUAL_CSTR("one", buffer);
687+
688+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(23, buffer, sizeof(buffer)));
689+
ASSUME_ITS_EQUAL_CSTR("twenty-three", buffer);
690+
691+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(100, buffer, sizeof(buffer)));
692+
ASSUME_ITS_EQUAL_CSTR("one hundred", buffer);
693+
694+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_number_to_words(1234, buffer, sizeof(buffer)));
695+
ASSUME_ITS_EQUAL_CSTR("one thousand two hundred thirty-four", buffer);
696+
}
697+
698+
FOSSIL_TEST(cpp_test_cstring_number_to_words_buffer_too_small) {
699+
char buffer[8];
700+
ASSUME_ITS_TRUE(fossil_io_cstring_number_to_words(123456, buffer, sizeof(buffer)) != 0);
701+
}
702+
703+
FOSSIL_TEST(cpp_test_cstring_class_number_from_words_and_to_words) {
704+
using fossil::io::CString;
705+
ASSUME_ITS_EQUAL_I32(23, CString::number_from_words("twenty-three"));
706+
ASSUME_ITS_EQUAL_I32(100, CString::number_from_words("one hundred"));
707+
ASSUME_ITS_EQUAL_I32(0, CString::number_from_words("zero"));
708+
709+
ASSUME_ITS_EQUAL_CSTR("twenty-three", CString::number_to_words(23).c_str());
710+
ASSUME_ITS_EQUAL_CSTR("one hundred", CString::number_to_words(100).c_str());
711+
ASSUME_ITS_EQUAL_CSTR("zero", CString::number_to_words(0).c_str());
712+
}
713+
714+
FOSSIL_TEST(cpp_test_cstring_class_number_from_words_exception) {
715+
using fossil::io::CString;
716+
bool thrown = false;
717+
try {
718+
CString::number_from_words("not-a-number");
719+
} catch (const std::invalid_argument&) {
720+
thrown = true;
721+
}
722+
ASSUME_ITS_TRUE(thrown);
723+
}
724+
725+
FOSSIL_TEST(cpp_test_cstring_class_number_to_words_exception) {
726+
using fossil::io::CString;
727+
bool thrown = false;
728+
try {
729+
// Use a very large number that would overflow the buffer
730+
CString::number_to_words(1234567890);
731+
} catch (const std::runtime_error&) {
732+
thrown = true;
733+
}
734+
ASSUME_ITS_TRUE(thrown);
735+
}
736+
652737
// * * * * * * * * * * * * * * * * * * * * * * * *
653738
// * Fossil Logic Test Pool
654739
// * * * * * * * * * * * * * * * * * * * * * * * *
@@ -732,6 +817,14 @@ FOSSIL_TEST_GROUP(cpp_string_tests) {
732817
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_strip_quotes);
733818
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_append);
734819

820+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_from_words_basic);
821+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_from_words_invalid);
822+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_to_words_basic);
823+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_number_to_words_buffer_too_small);
824+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_number_from_words_and_to_words);
825+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_number_from_words_exception);
826+
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_class_number_to_words_exception);
827+
735828
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_stream_class_create_and_free);
736829
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_stream_class_write_and_read);
737830
FOSSIL_TEST_ADD(cpp_string_suite, cpp_test_cstring_stream_class_multiple_writes);

0 commit comments

Comments
 (0)