Skip to content
193 changes: 190 additions & 3 deletions code/logic/cstring.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
*/
#include "fossil/io/cstring.h"
#include "fossil/io/output.h"
#include <string.h> // For strlen, strnlen, strncasecmp
#include <strings.h> // For strncasecmp on POSIX
#include <strings.h>
#include <stdlib.h>
#include <ctype.h> // For toupper, tolower
#include <string.h>
#include <locale.h>
#include <ctype.h>
#include <time.h>
#include <math.h>

#ifndef HAVE_STRNLEN
size_t strnlen(const char *s, size_t maxlen) {
Expand Down Expand Up @@ -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;
Expand Down
96 changes: 96 additions & 0 deletions code/logic/fossil/io/cstring.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down Expand Up @@ -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 &currency) {
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.
*
Expand Down
35 changes: 34 additions & 1 deletion code/tests/cases/test_cstring.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
// * * * * * * * * * * * * * * * * * * * * * * * *
Expand Down Expand Up @@ -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);
Expand Down
Loading