diff --git a/src/Infobox/CurrencyConversion.c b/src/Infobox/CurrencyConversion.c new file mode 100644 index 0000000..f9d1fa6 --- /dev/null +++ b/src/Infobox/CurrencyConversion.c @@ -0,0 +1,474 @@ +#include "CurrencyConversion.h" +#include "../Cache/Cache.h" +#include "../Utility/HttpClient.h" +#include "../Utility/JsonHelper.h" +#include +#include +#include +#include +#include + +typedef struct { + const char *code; + const char *name; + const char *symbol; + int is_crypto; +} CurrencyDef; + +static const CurrencyDef CURRENCIES[] = { + {"USD", "US Dollar", "$", 0}, + {"EUR", "Euro", "€", 0}, + {"GBP", "British Pound", "£", 0}, + {"JPY", "Japanese Yen", "¥", 0}, + {"AUD", "Australian Dollar", "A$", 0}, + {"CAD", "Canadian Dollar", "C$", 0}, + {"CHF", "Swiss Franc", "CHF", 0}, + {"CNY", "Chinese Yuan", "¥", 0}, + {"INR", "Indian Rupee", "₹", 0}, + {"KRW", "South Korean Won", "₩", 0}, + {"BRL", "Brazilian Real", "R$", 0}, + {"RUB", "Russian Ruble", "₽", 0}, + {"ZAR", "South African Rand", "R", 0}, + {"MXN", "Mexican Peso", "MX$", 0}, + {"SGD", "Singapore Dollar", "S$", 0}, + {"HKD", "Hong Kong Dollar", "HK$", 0}, + {"NOK", "Norwegian Krone", "kr", 0}, + {"SEK", "Swedish Krona", "kr", 0}, + {"DKK", "Danish Krone", "kr", 0}, + {"NZD", "New Zealand Dollar", "NZ$", 0}, + {"TRY", "Turkish Lira", "₺", 0}, + {"PLN", "Polish Zloty", "zł", 0}, + {"THB", "Thai Baht", "฿", 0}, + {"IDR", "Indonesian Rupiah", "Rp", 0}, + {"HUF", "Hungarian Forint", "Ft", 0}, + {"CZK", "Czech Koruna", "Kč", 0}, + {"ILS", "Israeli Shekel", "₪", 0}, + {"CLP", "Chilean Peso", "CLP$", 0}, + {"PHP", "Philippine Peso", "₱", 0}, + {"AED", "UAE Dirham", "د.إ", 0}, + {"COP", "Colombian Peso", "COP$", 0}, + {"SAR", "Saudi Riyal", "﷼", 0}, + {"MYR", "Malaysian Ringgit", "RM", 0}, + {"RON", "Romanian Leu", "lei", 0}, + {"ARS", "Argentine Peso", "ARS$", 0}, + {"PKR", "Pakistani Rupee", "₨", 0}, + {"EGP", "Egyptian Pound", "£", 0}, + {"VND", "Vietnamese Dong", "₫", 0}, + {"NGN", "Nigerian Naira", "₦", 0}, + {"BDT", "Bangladeshi Taka", "৳", 0}, + {"UAH", "Ukrainian Hryvnia", "₴", 0}, + {"PEN", "Peruvian Sol", "S/", 0}, + {"BGN", "Bulgarian Lev", "лв", 0}, + {"HRK", "Croatian Kuna", "kn", 0}, + {"ISK", "Icelandic Krona", "kr", 0}, + {"MAD", "Moroccan Dirham", "د.م.", 0}, + {"KES", "Kenyan Shilling", "KSh", 0}, + {"QAR", "Qatari Riyal", "﷼", 0}, + {"KWD", "Kuwaiti Dinar", "د.ك", 0}, + {"BHD", "Bahraini Dinar", ".د.ب", 0}, + {"OMR", "Omani Rial", "﷼", 0}, + {"JOD", "Jordanian Dinar", "د.ا", 0}, + {"TWD", "Taiwan Dollar", "NT$", 0}, + + {"BTC", "Bitcoin", "₿", 1}, + {"ETH", "Ethereum", "Ξ", 1}, + {"USDT", "Tether", "₮", 1}, + {"BNB", "Binance Coin", "BNB", 1}, + {"XRP", "Ripple", "XRP", 1}, + {"USDC", "USD Coin", "$", 1}, + {"ADA", "Cardano", "ADA", 1}, + {"DOGE", "Dogecoin", "Ð", 1}, + {"SOL", "Solana", "SOL", 1}, + {"TRX", "Tron", "TRX", 1}, + {"DOT", "Polkadot", "DOT", 1}, + {"MATIC", "Polygon", "MATIC", 1}, + {"LTC", "Litecoin", "Ł", 1}, + {"SHIB", "Shiba Inu", "SHIB", 1}, + {"AVAX", "Avalanche", "AVAX", 1}, + {"LINK", "Chainlink", "LINK", 1}, + {"ATOM", "Cosmos", "ATOM", 1}, + {"XMR", "Monero", "XMR", 1}, + {"ETC", "Ethereum Classic", "ETC", 1}, + {"XLM", "Stellar", "XLM", 1}, + {"BCH", "Bitcoin Cash", "BCH", 1}, + {"ALGO", "Algorand", "ALGO", 1}, + {"VET", "VeChain", "VET", 1}, + {"FIL", "Filecoin", "FIL", 1}, + {"NEAR", "NEAR Protocol", "NEAR", 1}, + {"APT", "Aptos", "APT", 1}, + {"ARB", "Arbitrum", "ARB", 1}, + {"OP", "Optimism", "OP", 1}, + {"SAND", "The Sandbox", "SAND", 1}, + {"MANA", "Decentraland", "MANA", 1}, + {"AXS", "Axie Infinity", "AXS", 1}, + {"AAVE", "Aave", "AAVE", 1}, + {"MKR", "Maker", "MKR", 1}, + {"GRT", "The Graph", "GRT", 1}, + {"FTM", "Fantom", "FTM", 1}, + {"CRO", "Cronos", "CRO", 1}, + {"NEAR", "NEAR Protocol", "NEAR", 1}, + {"INJ", "Injective", "INJ", 1}, + {"RUNE", "THORChain", "RUNE", 1}, + {"LDO", "Lido DAO", "LDO", 1}, + {"QNT", "Quant", "QNT", 1}, + {"RNDR", "Render", "RNDR", 1}, + {"STX", "Stacks", "STX", 1}, + {"IMX", "Immutable X", "IMX", 1}, + {"SNX", "Synthetix", "SNX", 1}, + {"THETA", "Theta", "THETA", 1}, + {"XTZ", "Tezos", "XTZ", 1}, + {"EOS", "EOS", "EOS", 1}, + {"FLOW", "Flow", "FLOW", 1}, + {"CHZ", "Chiliz", "CHZ", 1}, + {"CRV", "Curve DAO", "CRV", 1}, + {"APE", "ApeCoin", "APE", 1}, + {"PEPE", "Pepe", "PEPE", 1}, + {"WIF", "dogwifhat", "WIF", 1}, + {"SUI", "Sui", "SUI", 1}, + {"SEI", "Sei", "SEI", 1}, + {"TIA", "Celestia", "TIA", 1}, + {"PYTH", "Pyth Network", "PYTH", 1}, + {"BONK", "Bonk", "BONK", 1}, + {"FET", "Fetch.ai", "FET", 1}, + {"AGIX", "SingularityNET", "AGIX", 1}, + {"RNDR", "Render Token", "RNDR", 1}, +}; + +static const int CURRENCY_COUNT = sizeof(CURRENCIES) / sizeof(CURRENCIES[0]); + +static int is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +static const CurrencyDef *find_currency(const char *str) { + if (!str || !*str) + return NULL; + + size_t len = strlen(str); + if (len < 2 || len > 10) + return NULL; + + char normalized[16] = {0}; + size_t j = 0; + for (size_t i = 0; i < len && j < 15; i++) { + normalized[j++] = toupper((unsigned char)str[i]); + } + normalized[j] = '\0'; + + for (int i = 0; i < CURRENCY_COUNT; i++) { + if (strcmp(normalized, CURRENCIES[i].code) == 0) { + return &CURRENCIES[i]; + } + } + return NULL; +} + +int is_currency_query(const char *query) { + if (!query) + return 0; + + const char *patterns[] = {" to ", " in ", " into ", " = ", " equals ", NULL}; + + int has_pattern = 0; + for (int i = 0; patterns[i]; i++) { + if (strstr(query, patterns[i])) { + has_pattern = 1; + break; + } + } + + if (!has_pattern) { + const char *last_space = strrchr(query, ' '); + if (last_space) { + const CurrencyDef *c = find_currency(last_space + 1); + if (c) { + const char *before = query; + while (*before && is_whitespace(*before)) + before++; + const char *num_end = before; + while (*num_end && + (isdigit(*num_end) || *num_end == '.' || *num_end == ',' || + *num_end == '-' || *num_end == '+')) { + num_end++; + } + if (num_end > before) + has_pattern = 1; + } + } + } + + return has_pattern; +} + +static double parse_value(const char **ptr) { + const char *p = *ptr; + while (*p && is_whitespace(*p)) + p++; + + double value = 0.0; + int has_decimal = 0; + double decimal_place = 1.0; + + if (*p == '-' || *p == '+') + p++; + + while (*p >= '0' && *p <= '9') { + value = value * 10 + (*p - '0'); + if (has_decimal) + decimal_place *= 10; + p++; + } + + if (*p == '.' || *p == ',') { + has_decimal = 1; + p++; + while (*p >= '0' && *p <= '9') { + value = value * 10 + (*p - '0'); + decimal_place *= 10; + p++; + } + } + + if (has_decimal) + value /= decimal_place; + + *ptr = p; + return value; +} + +static int parse_currency_query(const char *query, double *value, + const CurrencyDef **from_curr, + const CurrencyDef **to_curr) { + *value = 0; + *from_curr = NULL; + *to_curr = NULL; + + const char *value_end = query; + *value = parse_value(&value_end); + + if (value_end == query) + return 0; + + const char *p = value_end; + while (*p && is_whitespace(*p)) + p++; + + size_t remaining = strlen(p); + if (remaining < 2) + return 0; + + const char *to_keywords[] = {" to ", " in ", " into ", " -> ", + " → ", " = ", NULL}; + const char *to_pos = NULL; + size_t keyword_len = 0; + for (int i = 0; to_keywords[i]; i++) { + const char *found = strstr(p, to_keywords[i]); + if (found) { + to_pos = found + strlen(to_keywords[i]); + keyword_len = strlen(to_keywords[i]); + break; + } + } + + if (!to_pos) { + const char *last_space = strrchr(p, ' '); + if (last_space && last_space > p) { + char from_part[32] = {0}; + size_t len = last_space - p; + if (len < 31) { + strncpy(from_part, p, len); + *from_curr = find_currency(from_part); + if (*from_curr) { + *to_curr = find_currency(last_space + 1); + return *to_curr ? 1 : 0; + } + } + } + return 0; + } + + char from_part[32] = {0}; + size_t from_len = to_pos - p - keyword_len; + if (from_len > 31) + from_len = 31; + strncpy(from_part, p, from_len); + + char *end_from = from_part + from_len; + while (end_from > from_part && is_whitespace(end_from[-1])) + end_from--; + *end_from = '\0'; + + *from_curr = find_currency(from_part); + if (!*from_curr) { + return 0; + } + + while (*to_pos && is_whitespace(*to_pos)) + to_pos++; + + if (!*to_pos) + return 0; + + char to_part[32] = {0}; + size_t to_len = 0; + const char *tp = to_pos; + while (*tp && !is_whitespace(*tp) && to_len < 31) { + to_part[to_len++] = *tp++; + } + to_part[to_len] = '\0'; + + *to_curr = find_currency(to_part); + if (!*to_curr) { + char try_buf[32] = {0}; + strncpy(try_buf, to_pos, 31); + *to_curr = find_currency(try_buf); + } + + return *to_curr ? 1 : 0; +} + +static double get_rate_from_json(const char *json, const char *target_code) { + JsonFloatMap map; + if (json_parse_float_map(json, "rates", &map)) { + for (size_t i = 0; i < map.count; i++) { + if (strcmp(map.keys[i], target_code) == 0) { + return map.values[i]; + } + } + } + return 0; +} + +static void format_number(double val, char *buf, size_t bufsize) { + if (bufsize == 0) + return; + if (val == 0) { + snprintf(buf, bufsize, "0"); + return; + } + if (fabs(val) < 0.01 && fabs(val) > 0) { + snprintf(buf, bufsize, "%.6f", val); + } else if (fabs(val) < 1) { + snprintf(buf, bufsize, "%.4f", val); + char *p = buf + strlen(buf) - 1; + while (p > buf && (*p == '0' || *p == '.')) { + if (*p == '.') + break; + *p-- = '\0'; + } + } else if (fmod(val + 0.0001, 1.0) < 0.0002) { + snprintf(buf, bufsize, "%.0f", val); + } else { + snprintf(buf, bufsize, "%.2f", val); + char *p = buf + strlen(buf) - 1; + while (p > buf && (*p == '0' || *p == '.')) { + if (*p == '.') + break; + *p-- = '\0'; + } + } +} + +static char *build_html(double value, const CurrencyDef *from, + const CurrencyDef *to, double result) { + static char html[4096]; + char val_buf[64], res_buf[64]; + + format_number(value, val_buf, sizeof(val_buf)); + format_number(result, res_buf, sizeof(res_buf)); + + snprintf(html, sizeof(html), + "
" + "
" + "%s %s = %s %s
" + "
" + "1 %s = %.4f %s
" + "
", + val_buf, from->code, res_buf, to->code, from->code, result / value, + to->code); + + return html; +} + +InfoBox fetch_currency_data(const char *query) { + InfoBox info = {NULL, NULL, NULL, NULL}; + if (!query) + return info; + + double value = 0; + const CurrencyDef *from_curr = NULL; + const CurrencyDef *to_curr = NULL; + + if (!parse_currency_query(query, &value, &from_curr, &to_curr)) + return info; + if (!from_curr || !to_curr) + return info; + if (strcmp(from_curr->code, to_curr->code) == 0) + return info; + + char cache_key[128]; + snprintf(cache_key, sizeof(cache_key), "currency_%s_%s", from_curr->code, + to_curr->code); + + char *cached_data = NULL; + size_t cached_size = 0; + double rate = 0; + int use_cache = 0; + + int is_crypto = from_curr->is_crypto || to_curr->is_crypto; + + if (get_cache_ttl_infobox() > 0) { + if (cache_get(cache_key, get_cache_ttl_infobox(), &cached_data, + &cached_size) == 0 && + cached_data && cached_size > 0) { + if (is_crypto) { + rate = json_get_float(cached_data, to_curr->code); + } else { + rate = get_rate_from_json(cached_data, to_curr->code); + } + if (rate > 0) { + use_cache = 1; + } + free(cached_data); + } + } + + if (!use_cache) { + char url[512]; + + if (is_crypto) { + snprintf(url, sizeof(url), + "https://min-api.cryptocompare.com/data/price?fsym=%s&tsyms=%s", + from_curr->code, to_curr->code); + } else { + snprintf(url, sizeof(url), + "https://api.exchangerate-api.com/v4/latest/%s", + from_curr->code); + } + + HttpResponse resp = http_get(url, "libcurl-agent/1.0"); + if (resp.memory && resp.size > 0) { + if (is_crypto) { + rate = json_get_float(resp.memory, to_curr->code); + } else { + rate = get_rate_from_json(resp.memory, to_curr->code); + } + if (rate > 0 && get_cache_ttl_infobox() > 0) { + cache_set(cache_key, resp.memory, resp.size); + } + } + http_response_free(&resp); + } + + if (rate <= 0) + return info; + + double result = value * rate; + + info.title = strdup("Currency Conversion"); + info.extract = strdup(build_html(value, from_curr, to_curr, result)); + info.thumbnail_url = strdup("/static/calculation.svg"); + info.url = strdup("#"); + + return info; +} diff --git a/src/Infobox/CurrencyConversion.h b/src/Infobox/CurrencyConversion.h new file mode 100644 index 0000000..f258fbe --- /dev/null +++ b/src/Infobox/CurrencyConversion.h @@ -0,0 +1,9 @@ +#ifndef CURRENCYCONVERSION_H +#define CURRENCYCONVERSION_H + +#include "Infobox.h" + +int is_currency_query(const char *query); +InfoBox fetch_currency_data(const char *query); + +#endif diff --git a/src/Routes/Search.c b/src/Routes/Search.c index dcfdf42..7d62f08 100644 --- a/src/Routes/Search.c +++ b/src/Routes/Search.c @@ -1,5 +1,6 @@ #include "Search.h" #include "../Infobox/Calculator.h" +#include "../Infobox/CurrencyConversion.h" #include "../Infobox/Dictionary.h" #include "../Infobox/UnitConversion.h" #include "../Infobox/Wikipedia.h" @@ -129,6 +130,20 @@ static void *unit_thread_func(void *arg) { return NULL; } +static void *currency_thread_func(void *arg) { + InfoBoxThreadData *data = (InfoBoxThreadData *)arg; + + if (is_currency_query(data->query)) { + data->result = fetch_currency_data(data->query); + data->success = + (data->result.title != NULL && data->result.extract != NULL); + } else { + data->success = 0; + } + + return NULL; +} + static int add_infobox_to_collection(InfoBox *infobox, char ****collection, int **inner_counts, int current_count) { *collection = @@ -182,17 +197,19 @@ int results_handler(UrlParams *params) { return -1; } - pthread_t wiki_tid, calc_tid, dict_tid, unit_tid; + pthread_t wiki_tid, calc_tid, dict_tid, unit_tid, currency_tid; InfoBoxThreadData wiki_data = {.query = raw_query, .success = 0}; InfoBoxThreadData calc_data = {.query = raw_query, .success = 0}; InfoBoxThreadData dict_data = {.query = raw_query, .success = 0}; InfoBoxThreadData unit_data = {.query = raw_query, .success = 0}; + InfoBoxThreadData currency_data = {.query = raw_query, .success = 0}; if (page == 1) { pthread_create(&wiki_tid, NULL, wiki_thread_func, &wiki_data); pthread_create(&calc_tid, NULL, calc_thread_func, &calc_data); pthread_create(&dict_tid, NULL, dict_thread_func, &dict_data); pthread_create(&unit_tid, NULL, unit_thread_func, &unit_data); + pthread_create(¤cy_tid, NULL, currency_thread_func, ¤cy_data); } ScrapeJob jobs[ENGINE_COUNT]; @@ -219,6 +236,7 @@ int results_handler(UrlParams *params) { pthread_join(calc_tid, NULL); pthread_join(dict_tid, NULL); pthread_join(unit_tid, NULL); + pthread_join(currency_tid, NULL); } char ***infobox_matrix = NULL; @@ -244,6 +262,12 @@ int results_handler(UrlParams *params) { &infobox_inner_counts, infobox_count); } + if (currency_data.success) { + infobox_count = + add_infobox_to_collection(¤cy_data.result, &infobox_matrix, + &infobox_inner_counts, infobox_count); + } + if (wiki_data.success) { infobox_count = add_infobox_to_collection(&wiki_data.result, &infobox_matrix, @@ -353,6 +377,8 @@ int results_handler(UrlParams *params) { free_infobox(&dict_data.result); if (unit_data.success) free_infobox(&unit_data.result); + if (currency_data.success) + free_infobox(¤cy_data.result); } free_context(&ctx); diff --git a/src/Utility/JsonHelper.c b/src/Utility/JsonHelper.c new file mode 100644 index 0000000..1eb4098 --- /dev/null +++ b/src/Utility/JsonHelper.c @@ -0,0 +1,139 @@ +#include "JsonHelper.h" +#include +#include +#include +#include + +static int is_whitespace_json(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +int json_parse_float_map(const char *json, const char *target_key, + JsonFloatMap *result) { + memset(result, 0, sizeof(JsonFloatMap)); + + if (!json || !target_key) + return 0; + + const char *obj_start = strchr(json, '{'); + if (!obj_start) + return 0; + + const char *rates_start = strstr(obj_start, target_key); + if (!rates_start) + return 0; + + rates_start = strchr(rates_start, '{'); + if (!rates_start) + return 0; + + const char *p = rates_start + 1; + while (*p && *p != '}') { + while (*p && (*p == ' ' || *p == '\n' || *p == '\t' || *p == ',')) + p++; + + if (*p == '}') + break; + + if (*p != '"') + break; + p++; + + char key[32] = {0}; + size_t key_len = 0; + while (*p && *p != '"' && key_len < 31) { + key[key_len++] = *p++; + } + if (*p != '"') + break; + p++; + + while (*p && *p != ':') + p++; + if (*p != ':') + break; + p++; + + while (*p && is_whitespace_json(*p)) + p++; + + double value = 0; + int has_digit = 0; + while (*p >= '0' && *p <= '9') { + value = value * 10 + (*p - '0'); + has_digit = 1; + p++; + } + if (*p == '.') { + p++; + double frac = 0.1; + while (*p >= '0' && *p <= '9') { + value += (*p - '0') * frac; + frac *= 0.1; + has_digit = 1; + p++; + } + } + + if (has_digit && key_len > 0) { + memcpy(result->keys[result->count], key, key_len); + result->keys[result->count][key_len] = '\0'; + result->values[result->count] = value; + result->count++; + if (result->count >= 256) + break; + } + } + + return result->count > 0; +} + +double json_get_float(const char *json, const char *key) { + if (!json || !key) + return 0; + + const char *key_pos = strstr(json, key); + if (!key_pos) + return 0; + + const char *colon = strchr(key_pos, ':'); + if (!colon) + return 0; + + colon++; + while (*colon && is_whitespace_json(*colon)) + colon++; + + return strtod(colon, NULL); +} + +char *json_get_string(const char *json, const char *key) { + if (!json || !key) + return NULL; + + static char buffer[256]; + + const char *key_pos = strstr(json, key); + if (!key_pos) + return NULL; + + const char *colon = strchr(key_pos, ':'); + if (!colon) + return NULL; + + colon++; + while (*colon && is_whitespace_json(*colon)) + colon++; + + if (*colon != '"') + return NULL; + colon++; + + size_t len = 0; + while (*colon && *colon != '"' && len < 255) { + buffer[len++] = *colon++; + } + buffer[len] = '\0'; + + return buffer; +} diff --git a/src/Utility/JsonHelper.h b/src/Utility/JsonHelper.h new file mode 100644 index 0000000..62034b8 --- /dev/null +++ b/src/Utility/JsonHelper.h @@ -0,0 +1,17 @@ +#ifndef JSONHELPER_H +#define JSONHELPER_H + +#include + +typedef struct { + double values[256]; + char keys[256][32]; + size_t count; +} JsonFloatMap; + +int json_parse_float_map(const char *json, const char *target_key, + JsonFloatMap *result); +double json_get_float(const char *json, const char *key); +char *json_get_string(const char *json, const char *key); + +#endif