Skip to content

Commit 938f304

Browse files
Merge pull request #70 from dreamer-coding/cstring_patch_addon
2 parents cf00ecf + 827baeb commit 938f304

File tree

3 files changed

+320
-4
lines changed

3 files changed

+320
-4
lines changed

code/logic/cstring.c

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
*/
1414
#include "fossil/io/cstring.h"
1515
#include "fossil/io/output.h"
16-
#include <string.h> // For strlen, strnlen, strncasecmp
17-
#include <strings.h> // For strncasecmp on POSIX
16+
#include <strings.h>
1817
#include <stdlib.h>
19-
#include <ctype.h> // For toupper, tolower
18+
#include <string.h>
19+
#include <locale.h>
20+
#include <ctype.h>
2021
#include <time.h>
22+
#include <math.h>
2123

2224
#ifndef HAVE_STRNLEN
2325
size_t strnlen(const char *s, size_t maxlen) {
@@ -57,6 +59,191 @@ void fossil_io_cstring_free(cstring str) {
5759
}
5860
}
5961

62+
// ---------------------------------------
63+
// Locale-Aware Money String Conversions
64+
// ---------------------------------------
65+
66+
int fossil_io_cstring_money_to_string(double amount, char *output, size_t size) {
67+
if (!output || size == 0) return -1;
68+
69+
// Set locale temporarily to the user's default locale
70+
char *old_locale = setlocale(LC_NUMERIC, NULL);
71+
setlocale(LC_NUMERIC, "");
72+
73+
amount = round(amount * 100.0) / 100.0; // Round to 2 decimals
74+
75+
char temp[64];
76+
int written = snprintf(temp, sizeof(temp), "%.2f", fabs(amount));
77+
if (written < 0 || written >= (int)sizeof(temp)) return -1;
78+
79+
// Determine locale decimal and thousand separators
80+
struct lconv *lc = localeconv();
81+
char decimal_sep = lc && lc->decimal_point ? lc->decimal_point[0] : '.';
82+
char thousand_sep = lc && lc->thousands_sep ? lc->thousands_sep[0] : ',';
83+
84+
// Replace decimal point with locale decimal separator
85+
char *dot = strchr(temp, '.');
86+
if (dot) *dot = decimal_sep;
87+
88+
int int_len = dot ? (int)(dot - temp) : (int)strlen(temp);
89+
int commas = (int_len - 1) / 3;
90+
int total_len = int_len + commas + (dot ? strlen(dot) : 0);
91+
92+
if ((size_t)(total_len + 3) > size) return -1;
93+
94+
char formatted[128];
95+
int fpos = 0;
96+
97+
if (amount < 0) formatted[fpos++] = '-';
98+
formatted[fpos++] = '$'; // Keep USD-style symbol
99+
100+
int leading = int_len % 3;
101+
if (leading == 0) leading = 3;
102+
103+
for (int i = 0; i < int_len; i++) {
104+
formatted[fpos++] = temp[i];
105+
if ((i + 1) % leading == 0 && (i + 1) < int_len) {
106+
formatted[fpos++] = thousand_sep;
107+
leading = 3;
108+
}
109+
}
110+
111+
if (dot) {
112+
strcpy(&formatted[fpos], dot);
113+
fpos += strlen(dot);
114+
}
115+
116+
formatted[fpos] = '\0';
117+
strncpy(output, formatted, size - 1);
118+
output[size - 1] = '\0';
119+
120+
// Restore previous locale
121+
setlocale(LC_NUMERIC, old_locale);
122+
123+
return 0;
124+
}
125+
126+
int fossil_io_cstring_string_to_money(const char *input, double *amount) {
127+
if (!input || !amount) return -1;
128+
129+
char buffer[128];
130+
size_t j = 0;
131+
int negative = 0;
132+
133+
// Skip leading spaces
134+
while (isspace((unsigned char)*input)) input++;
135+
136+
if (*input == '(') {
137+
negative = 1;
138+
input++;
139+
}
140+
141+
struct lconv *lc = localeconv();
142+
char decimal_sep = lc && lc->decimal_point ? lc->decimal_point[0] : '.';
143+
144+
// Copy digits and decimal separator only
145+
for (size_t i = 0; input[i] && j < sizeof(buffer) - 1; i++) {
146+
if (isdigit((unsigned char)input[i]) || input[i] == decimal_sep) {
147+
buffer[j++] = input[i];
148+
}
149+
}
150+
buffer[j] = '\0';
151+
152+
if (j == 0) return -1;
153+
154+
// Replace locale decimal with '.' for atof
155+
for (size_t i = 0; i < j; i++) {
156+
if (buffer[i] == decimal_sep) buffer[i] = '.';
157+
}
158+
159+
*amount = atof(buffer);
160+
if (negative || strchr(input, '-')) *amount = -*amount;
161+
162+
return 0;
163+
}
164+
165+
int fossil_io_cstring_money_to_string_currency(double amount, char *output, size_t size, const char *currency) {
166+
if (!output || size == 0) return -1;
167+
if (!currency) currency = "$";
168+
169+
amount = round(amount * 100.0) / 100.0; // Round to 2 decimals
170+
171+
char temp[64];
172+
int written = snprintf(temp, sizeof(temp), "%.2f", fabs(amount));
173+
if (written < 0 || written >= (int)sizeof(temp)) return -1;
174+
175+
// Replace decimal point with '.'
176+
char *dot = strchr(temp, '.');
177+
int int_len = dot ? (int)(dot - temp) : (int)strlen(temp);
178+
int commas = (int_len - 1) / 3;
179+
int total_len = int_len + commas + (dot ? strlen(dot) : 0);
180+
181+
if ((size_t)(total_len + strlen(currency) + 2) > size) return -1;
182+
183+
char formatted[128];
184+
int fpos = 0;
185+
186+
if (amount < 0) formatted[fpos++] = '-';
187+
strcpy(&formatted[fpos], currency);
188+
fpos += strlen(currency);
189+
190+
int leading = int_len % 3;
191+
if (leading == 0) leading = 3;
192+
193+
for (int i = 0; i < int_len; i++) {
194+
formatted[fpos++] = temp[i];
195+
if ((i + 1) % leading == 0 && (i + 1) < int_len) {
196+
formatted[fpos++] = ',';
197+
leading = 3;
198+
}
199+
}
200+
201+
if (dot) {
202+
strcpy(&formatted[fpos], dot);
203+
fpos += strlen(dot);
204+
}
205+
206+
formatted[fpos] = '\0';
207+
strncpy(output, formatted, size - 1);
208+
output[size - 1] = '\0';
209+
210+
return 0;
211+
}
212+
213+
int fossil_io_cstring_string_to_money_currency(const char *input, double *amount) {
214+
if (!input || !amount) return -1;
215+
216+
char buffer[128];
217+
size_t j = 0;
218+
int negative = 0;
219+
220+
while (isspace((unsigned char)*input)) input++;
221+
222+
if (*input == '(') {
223+
negative = 1;
224+
input++;
225+
}
226+
227+
if (!isdigit((unsigned char)*input) && *input != '-' && *input != '.') {
228+
// Skip currency symbol
229+
input++;
230+
}
231+
232+
for (size_t i = 0; input[i] && j < sizeof(buffer) - 1; i++) {
233+
if (isdigit((unsigned char)input[i]) || input[i] == '.') {
234+
buffer[j++] = input[i];
235+
}
236+
}
237+
buffer[j] = '\0';
238+
239+
if (j == 0) return -1;
240+
241+
*amount = atof(buffer);
242+
if (negative || strchr(input, '-')) *amount = -*amount;
243+
244+
return 0;
245+
}
246+
60247
// ---------------- Tokenizer ----------------
61248
cstring fossil_io_cstring_token(cstring str, ccstring delim, cstring *saveptr) {
62249
if (!saveptr || (!str && !*saveptr)) return NULL;

code/logic/fossil/io/cstring.h

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,57 @@ cstring fossil_io_cstring_create(ccstring init);
4949
*/
5050
void fossil_io_cstring_free(cstring str);
5151

52+
// Money String Conversions
53+
54+
/**
55+
* @brief Converts a double amount into a formatted money string.
56+
*
57+
* Example: 1234.56 -> "$1,234.56"
58+
*
59+
* @param amount The numeric amount to convert.
60+
* @param output Buffer to store the formatted string.
61+
* @param size Size of the output buffer.
62+
* @return 0 on success, -1 if the buffer is too small or invalid.
63+
*/
64+
int fossil_io_cstring_money_to_string(double amount, cstring output, size_t size);
65+
66+
/**
67+
* @brief Parses a money string into a numeric double value.
68+
*
69+
* Example: "$1,234.56" -> 1234.56
70+
*
71+
* @param input Input string representing money.
72+
* @param amount Pointer to store the parsed numeric value.
73+
* @return 0 on success, -1 on failure (invalid format).
74+
*/
75+
int fossil_io_cstring_string_to_money(ccstring input, double *amount);
76+
77+
/**
78+
* @brief Converts a double amount into a formatted money string with optional currency symbol.
79+
*
80+
* Example: 1234.56 -> "$1,234.56" (USD default)
81+
*
82+
* @param amount The numeric amount to convert.
83+
* @param output Buffer to store the formatted string.
84+
* @param size Size of the output buffer.
85+
* @param currency Currency symbol to prepend (e.g., "$", "€", "¥"); NULL defaults to "$".
86+
* @return 0 on success, -1 if the buffer is too small or invalid.
87+
*/
88+
int fossil_io_cstring_money_to_string_currency(double amount, char *output, size_t size, const char *currency);
89+
90+
/**
91+
* @brief Parses a money string into a numeric double value.
92+
*
93+
* Detects and ignores a currency symbol at the start.
94+
*
95+
* Example: "$1,234.56" -> 1234.56
96+
*
97+
* @param input Input string representing money.
98+
* @param amount Pointer to store the parsed numeric value.
99+
* @return 0 on success, -1 on failure (invalid format).
100+
*/
101+
int fossil_io_cstring_string_to_money_currency(const char *input, double *amount);
102+
52103
/**
53104
* @brief Tokenizes a string by delimiters (reentrant version).
54105
*
@@ -1078,6 +1129,51 @@ namespace fossil {
10781129
return std::string(buffer);
10791130
}
10801131

1132+
/**
1133+
* Convert numeric amount to string.
1134+
* Uses default currency "$".
1135+
*/
1136+
static std::string money_to_string(double amount) {
1137+
char buffer[128];
1138+
if (fossil_io_cstring_money_to_string(amount, buffer, sizeof(buffer)) != 0) {
1139+
throw std::runtime_error("Failed to convert amount to string");
1140+
}
1141+
return std::string(buffer);
1142+
}
1143+
1144+
/**
1145+
* Convert numeric amount to string with currency symbol.
1146+
*/
1147+
static std::string currency_to_string(double amount, const std::string &currency) {
1148+
char buffer[128];
1149+
if (fossil_io_cstring_money_to_string_currency(amount, buffer, sizeof(buffer), currency.c_str()) != 0) {
1150+
throw std::runtime_error("Failed to convert amount to string with currency");
1151+
}
1152+
return std::string(buffer);
1153+
}
1154+
1155+
/**
1156+
* Convert string to numeric amount.
1157+
*/
1158+
static double from_money(const std::string &str) {
1159+
double value = 0.0;
1160+
if (fossil_io_cstring_string_to_money(str.c_str(), &value) != 0) {
1161+
throw std::runtime_error("Failed to parse money string");
1162+
}
1163+
return value;
1164+
}
1165+
1166+
/**
1167+
* Convert string to numeric amount with currency detection.
1168+
*/
1169+
static double from_currency(const std::string &str) {
1170+
double value = 0.0;
1171+
if (fossil_io_cstring_string_to_money_currency(str.c_str(), &value) != 0) {
1172+
throw std::runtime_error("Failed to parse money string with currency");
1173+
}
1174+
return value;
1175+
}
1176+
10811177
/**
10821178
* Creates a copy of the given cstring.
10831179
*

code/tests/cases/test_cstring.c

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
// Define the test suite and add test cases
2727
FOSSIL_SUITE(c_string_suite);
28-
fossil_fstream_t c_string;
2928

3029
// Setup function for the test suite
3130
FOSSIL_SETUP(c_string_suite) {
@@ -774,6 +773,37 @@ FOSSIL_TEST(c_test_cstring_number_to_words) {
774773
ASSUME_ITS_TRUE(fossil_io_cstring_number_to_words(123456789, buffer, 5) != 0);
775774
}
776775

776+
// Test fossil_io_cstring_string_to_money with tolerance
777+
FOSSIL_TEST(c_test_cstring_string_to_money) {
778+
double value;
779+
780+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money("$1,234.56", &value));
781+
ASSUME_ITS_EQUAL_F64(value, (double)1234.56, (double)0.001);
782+
783+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money("-$42.50", &value));
784+
ASSUME_ITS_EQUAL_F64(value, (double)-42.50, (double)0.001);
785+
786+
// Invalid string
787+
ASSUME_ITS_TRUE(fossil_io_cstring_string_to_money("foobar", &value) != 0);
788+
}
789+
790+
// Test fossil_io_cstring_string_to_money_currency with tolerance
791+
FOSSIL_TEST(c_test_cstring_string_to_money_currency) {
792+
double value;
793+
794+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money_currency("$1,234.56", &value));
795+
ASSUME_ITS_EQUAL_F64(value, (double)1234.56, (double)0.001);
796+
797+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money_currency("€987.65", &value));
798+
ASSUME_ITS_EQUAL_F64(value, (double)987.65, (double)0.001);
799+
800+
ASSUME_ITS_EQUAL_I32(0, fossil_io_cstring_string_to_money_currency("-$42.50", &value));
801+
ASSUME_ITS_EQUAL_F64(value, (double)-42.50, (double)0.001);
802+
803+
// Invalid format
804+
ASSUME_ITS_TRUE(fossil_io_cstring_string_to_money_currency("foobar", &value) != 0);
805+
}
806+
777807
// * * * * * * * * * * * * * * * * * * * * * * * *
778808
// * Fossil Logic Test Pool
779809
// * * * * * * * * * * * * * * * * * * * * * * * *
@@ -855,6 +885,9 @@ FOSSIL_TEST_GROUP(c_string_tests) {
855885
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_strip_quotes_safe);
856886
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_normalize_spaces_safe);
857887
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_index_of_safe);
888+
889+
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_string_to_money);
890+
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_string_to_money_currency);
858891

859892
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_create_and_free);
860893
FOSSIL_TEST_ADD(c_string_suite, c_test_cstring_stream_write_and_read);

0 commit comments

Comments
 (0)