diff --git a/code/logic/cstring.c b/code/logic/cstring.c index 25177b6..456e78d 100644 --- a/code/logic/cstring.c +++ b/code/logic/cstring.c @@ -13,11 +13,13 @@ */ #include "fossil/io/cstring.h" #include "fossil/io/output.h" -#include // For strlen, strnlen, strncasecmp -#include // For strncasecmp on POSIX +#include #include -#include // For toupper, tolower +#include +#include +#include #include +#include #ifndef HAVE_STRNLEN size_t strnlen(const char *s, size_t maxlen) { @@ -57,6 +59,191 @@ void fossil_io_cstring_free(cstring str) { } } +// --------------------------------------- +// Locale-Aware Money String Conversions +// --------------------------------------- + +int fossil_io_cstring_money_to_string(double amount, char *output, size_t size) { + if (!output || size == 0) return -1; + + // Set locale temporarily to the user's default locale + char *old_locale = setlocale(LC_NUMERIC, NULL); + setlocale(LC_NUMERIC, ""); + + amount = round(amount * 100.0) / 100.0; // Round to 2 decimals + + char temp[64]; + int written = snprintf(temp, sizeof(temp), "%.2f", fabs(amount)); + if (written < 0 || written >= (int)sizeof(temp)) return -1; + + // Determine locale decimal and thousand separators + struct lconv *lc = localeconv(); + char decimal_sep = lc && lc->decimal_point ? lc->decimal_point[0] : '.'; + char thousand_sep = lc && lc->thousands_sep ? lc->thousands_sep[0] : ','; + + // Replace decimal point with locale decimal separator + char *dot = strchr(temp, '.'); + if (dot) *dot = decimal_sep; + + int int_len = dot ? (int)(dot - temp) : (int)strlen(temp); + int commas = (int_len - 1) / 3; + int total_len = int_len + commas + (dot ? strlen(dot) : 0); + + if ((size_t)(total_len + 3) > size) return -1; + + char formatted[128]; + int fpos = 0; + + if (amount < 0) formatted[fpos++] = '-'; + formatted[fpos++] = '$'; // Keep USD-style symbol + + int leading = int_len % 3; + if (leading == 0) leading = 3; + + for (int i = 0; i < int_len; i++) { + formatted[fpos++] = temp[i]; + if ((i + 1) % leading == 0 && (i + 1) < int_len) { + formatted[fpos++] = thousand_sep; + leading = 3; + } + } + + if (dot) { + strcpy(&formatted[fpos], dot); + fpos += strlen(dot); + } + + formatted[fpos] = '\0'; + strncpy(output, formatted, size - 1); + output[size - 1] = '\0'; + + // Restore previous locale + setlocale(LC_NUMERIC, old_locale); + + return 0; +} + +int fossil_io_cstring_string_to_money(const char *input, double *amount) { + if (!input || !amount) return -1; + + char buffer[128]; + size_t j = 0; + int negative = 0; + + // Skip leading spaces + while (isspace((unsigned char)*input)) input++; + + if (*input == '(') { + negative = 1; + input++; + } + + struct lconv *lc = localeconv(); + char decimal_sep = lc && lc->decimal_point ? lc->decimal_point[0] : '.'; + + // Copy digits and decimal separator only + for (size_t i = 0; input[i] && j < sizeof(buffer) - 1; i++) { + if (isdigit((unsigned char)input[i]) || input[i] == decimal_sep) { + buffer[j++] = input[i]; + } + } + buffer[j] = '\0'; + + if (j == 0) return -1; + + // Replace locale decimal with '.' for atof + for (size_t i = 0; i < j; i++) { + if (buffer[i] == decimal_sep) buffer[i] = '.'; + } + + *amount = atof(buffer); + if (negative || strchr(input, '-')) *amount = -*amount; + + return 0; +} + +int fossil_io_cstring_money_to_string_currency(double amount, char *output, size_t size, const char *currency) { + if (!output || size == 0) return -1; + if (!currency) currency = "$"; + + amount = round(amount * 100.0) / 100.0; // Round to 2 decimals + + char temp[64]; + int written = snprintf(temp, sizeof(temp), "%.2f", fabs(amount)); + if (written < 0 || written >= (int)sizeof(temp)) return -1; + + // Replace decimal point with '.' + char *dot = strchr(temp, '.'); + int int_len = dot ? (int)(dot - temp) : (int)strlen(temp); + int commas = (int_len - 1) / 3; + int total_len = int_len + commas + (dot ? strlen(dot) : 0); + + if ((size_t)(total_len + strlen(currency) + 2) > size) return -1; + + char formatted[128]; + int fpos = 0; + + if (amount < 0) formatted[fpos++] = '-'; + strcpy(&formatted[fpos], currency); + fpos += strlen(currency); + + int leading = int_len % 3; + if (leading == 0) leading = 3; + + for (int i = 0; i < int_len; i++) { + formatted[fpos++] = temp[i]; + if ((i + 1) % leading == 0 && (i + 1) < int_len) { + formatted[fpos++] = ','; + leading = 3; + } + } + + if (dot) { + strcpy(&formatted[fpos], dot); + fpos += strlen(dot); + } + + formatted[fpos] = '\0'; + strncpy(output, formatted, size - 1); + output[size - 1] = '\0'; + + return 0; +} + +int fossil_io_cstring_string_to_money_currency(const char *input, double *amount) { + if (!input || !amount) return -1; + + char buffer[128]; + size_t j = 0; + int negative = 0; + + while (isspace((unsigned char)*input)) input++; + + if (*input == '(') { + negative = 1; + input++; + } + + if (!isdigit((unsigned char)*input) && *input != '-' && *input != '.') { + // Skip currency symbol + input++; + } + + for (size_t i = 0; input[i] && j < sizeof(buffer) - 1; i++) { + if (isdigit((unsigned char)input[i]) || input[i] == '.') { + buffer[j++] = input[i]; + } + } + buffer[j] = '\0'; + + if (j == 0) return -1; + + *amount = atof(buffer); + if (negative || strchr(input, '-')) *amount = -*amount; + + return 0; +} + // ---------------- Tokenizer ---------------- cstring fossil_io_cstring_token(cstring str, ccstring delim, cstring *saveptr) { if (!saveptr || (!str && !*saveptr)) return NULL; diff --git a/code/logic/fossil/io/cstring.h b/code/logic/fossil/io/cstring.h index 35b094b..fa8ec14 100644 --- a/code/logic/fossil/io/cstring.h +++ b/code/logic/fossil/io/cstring.h @@ -49,6 +49,57 @@ cstring fossil_io_cstring_create(ccstring init); */ void fossil_io_cstring_free(cstring str); +// Money String Conversions + +/** + * @brief Converts a double amount into a formatted money string. + * + * Example: 1234.56 -> "$1,234.56" + * + * @param amount The numeric amount to convert. + * @param output Buffer to store the formatted string. + * @param size Size of the output buffer. + * @return 0 on success, -1 if the buffer is too small or invalid. + */ +int fossil_io_cstring_money_to_string(double amount, cstring output, size_t size); + +/** + * @brief Parses a money string into a numeric double value. + * + * Example: "$1,234.56" -> 1234.56 + * + * @param input Input string representing money. + * @param amount Pointer to store the parsed numeric value. + * @return 0 on success, -1 on failure (invalid format). + */ +int fossil_io_cstring_string_to_money(ccstring input, double *amount); + +/** + * @brief Converts a double amount into a formatted money string with optional currency symbol. + * + * Example: 1234.56 -> "$1,234.56" (USD default) + * + * @param amount The numeric amount to convert. + * @param output Buffer to store the formatted string. + * @param size Size of the output buffer. + * @param currency Currency symbol to prepend (e.g., "$", "€", "¥"); NULL defaults to "$". + * @return 0 on success, -1 if the buffer is too small or invalid. + */ +int fossil_io_cstring_money_to_string_currency(double amount, char *output, size_t size, const char *currency); + +/** + * @brief Parses a money string into a numeric double value. + * + * Detects and ignores a currency symbol at the start. + * + * Example: "$1,234.56" -> 1234.56 + * + * @param input Input string representing money. + * @param amount Pointer to store the parsed numeric value. + * @return 0 on success, -1 on failure (invalid format). + */ +int fossil_io_cstring_string_to_money_currency(const char *input, double *amount); + /** * @brief Tokenizes a string by delimiters (reentrant version). * @@ -1078,6 +1129,51 @@ namespace fossil { return std::string(buffer); } + /** + * Convert numeric amount to string. + * Uses default currency "$". + */ + static std::string money_to_string(double amount) { + char buffer[128]; + if (fossil_io_cstring_money_to_string(amount, buffer, sizeof(buffer)) != 0) { + throw std::runtime_error("Failed to convert amount to string"); + } + return std::string(buffer); + } + + /** + * Convert numeric amount to string with currency symbol. + */ + static std::string currency_to_string(double amount, const std::string ¤cy) { + char buffer[128]; + if (fossil_io_cstring_money_to_string_currency(amount, buffer, sizeof(buffer), currency.c_str()) != 0) { + throw std::runtime_error("Failed to convert amount to string with currency"); + } + return std::string(buffer); + } + + /** + * Convert string to numeric amount. + */ + static double from_money(const std::string &str) { + double value = 0.0; + if (fossil_io_cstring_string_to_money(str.c_str(), &value) != 0) { + throw std::runtime_error("Failed to parse money string"); + } + return value; + } + + /** + * Convert string to numeric amount with currency detection. + */ + static double from_currency(const std::string &str) { + double value = 0.0; + if (fossil_io_cstring_string_to_money_currency(str.c_str(), &value) != 0) { + throw std::runtime_error("Failed to parse money string with currency"); + } + return value; + } + /** * Creates a copy of the given cstring. * diff --git a/code/tests/cases/test_cstring.c b/code/tests/cases/test_cstring.c index 79d93ba..a730118 100644 --- a/code/tests/cases/test_cstring.c +++ b/code/tests/cases/test_cstring.c @@ -25,7 +25,6 @@ // Define the test suite and add test cases FOSSIL_SUITE(c_string_suite); -fossil_fstream_t c_string; // Setup function for the test suite FOSSIL_SETUP(c_string_suite) { @@ -774,6 +773,37 @@ FOSSIL_TEST(c_test_cstring_number_to_words) { ASSUME_ITS_TRUE(fossil_io_cstring_number_to_words(123456789, buffer, 5) != 0); } +// Test fossil_io_cstring_string_to_money with tolerance +FOSSIL_TEST(c_test_cstring_string_to_money) { + double value; + + ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money("$1,234.56", &value)); + ASSUME_ITS_EQUAL_F64(value, (double)1234.56, (double)0.001); + + ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money("-$42.50", &value)); + ASSUME_ITS_EQUAL_F64(value, (double)-42.50, (double)0.001); + + // Invalid string + ASSUME_ITS_TRUE(fossil_io_cstring_string_to_money("foobar", &value) != 0); +} + +// Test fossil_io_cstring_string_to_money_currency with tolerance +FOSSIL_TEST(c_test_cstring_string_to_money_currency) { + double value; + + ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money_currency("$1,234.56", &value)); + ASSUME_ITS_EQUAL_F64(value, (double)1234.56, (double)0.001); + + ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money_currency("€987.65", &value)); + ASSUME_ITS_EQUAL_F64(value, (double)987.65, (double)0.001); + + ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money_currency("-$42.50", &value)); + ASSUME_ITS_EQUAL_F64(value, (double)-42.50, (double)0.001); + + // Invalid format + ASSUME_ITS_TRUE(fossil_io_cstring_string_to_money_currency("foobar", &value) != 0); +} + // * * * * * * * * * * * * * * * * * * * * * * * * // * Fossil Logic Test Pool // * * * * * * * * * * * * * * * * * * * * * * * * @@ -855,6 +885,9 @@ FOSSIL_TEST_GROUP(c_string_tests) { FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_strip_quotes_safe); FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_normalize_spaces_safe); FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_index_of_safe); + + FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_string_to_money); + FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_string_to_money_currency); 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);