Fix 0^n generating error for fractional n
[gcalctool.git] / src / currency-manager.c
blobc7687ba7204d0335a23305de334059ee560975ab
1 /*
2 * Copyright (C) 2008-2011 Robert Ancell.
4 * This program is free software: you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License as published by the Free Software
6 * Foundation, either version 2 of the License, or (at your option) any later
7 * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
8 * license.
9 */
11 #include <time.h>
13 #include <glib.h>
14 #include <glib/gstdio.h>
15 #include <gio/gio.h>
16 #include <libxml/tree.h>
17 #include <libxml/xpath.h>
18 #include <libxml/xpathInternals.h>
19 #include <glib/gi18n.h>
21 #include "currency-manager.h"
22 #include "mp.h"
24 typedef struct {
25 char *short_name;
26 char *symbol;
27 char *long_name;
28 } CurrencyInfo;
29 static const CurrencyInfo currency_info[] = {
30 {"AED", "إ.د", N_("UAE Dirham")},
31 {"AUD", "$", N_("Australian Dollar")},
32 {"BGN", "лв", N_("Bulgarian Lev")},
33 {"BHD", ".ب.د", N_("Bahraini Dinar")},
34 {"BND", "$", N_("Brunei Dollar")},
35 {"BRL", "R$", N_("Brazilian Real")},
36 {"BWP", "P", N_("Botswana Pula")},
37 {"CAD", "$", N_("Canadian Dollar")},
38 {"CHF", "Fr", N_("Swiss Franc")},
39 {"CLP", "$", N_("Chilean Peso")},
40 {"CNY", "元", N_("Chinese Yuan")},
41 {"COP", "$", N_("Colombian Peso")},
42 {"CZK", "Kč", N_("Czech Koruna")},
43 {"DKK", "kr", N_("Danish Krone")},
44 {"DZD", "ج.د", N_("Algerian Dinar")},
45 {"EEK", "KR", N_("Estonian Kroon")},
46 {"EUR", "€", N_("Euro")},
47 {"GBP", "£", N_("Pound Sterling")},
48 {"HKD", "$", N_("Hong Kong Dollar")},
49 {"HRK", "kn", N_("Croatian Kuna")},
50 {"HUF", "Ft", N_("Hungarian Forint")},
51 {"IDR", "Rp", N_("Indonesian Rupiah")},
52 {"ILS", "₪", N_("Israeli New Shekel")},
53 {"INR", "₹", N_("Indian Rupee")},
54 {"IRR", "﷼", N_("Iranian Rial")},
55 {"ISK", "kr", N_("Icelandic Krona")},
56 {"JPY", "¥", N_("Japanese Yen")},
57 {"KRW", "₩", N_("South Korean Won")},
58 {"KWD", "ك.د", N_("Kuwaiti Dinar")},
59 {"KZT", "₸", N_("Kazakhstani Tenge")},
60 {"LKR", "Rs", N_("Sri Lankan Rupee")},
61 {"LTL", "Lt", N_("Lithuanian Litas")},
62 {"LVL", "Ls", N_("Latvian Lats")},
63 {"LYD", "د.ل", N_("Libyan Dinar")},
64 {"MUR", "Rs", N_("Mauritian Rupee")},
65 {"MXN", "$", N_("Mexican Peso")},
66 {"MYR", "RM", N_("Malaysian Ringgit")},
67 {"NOK", "kr", N_("Norwegian Krone")},
68 {"NPR", "Rs", N_("Nepalese Rupee")},
69 {"NZD", "$", N_("New Zealand Dollar")},
70 {"OMR", "ع.ر.", N_("Omani Rial")},
71 {"PEN", "S/.", N_("Peruvian Nuevo Sol")},
72 {"PHP", "₱", N_("Philippine Peso")},
73 {"PKR", "Rs", N_("Pakistani Rupee")},
74 {"PLN", "zł", N_("Polish Zloty")},
75 {"QAR", "ق.ر", N_("Qatari Riyal")},
76 {"RON", "L", N_("New Romanian Leu")},
77 {"RUB", "руб.", N_("Russian Rouble")},
78 {"SAR", "س.ر", N_("Saudi Riyal")},
79 {"SEK", "kr", N_("Swedish Krona")},
80 {"SGD", "$", N_("Singapore Dollar")},
81 {"THB", "฿", N_("Thai Baht")},
82 {"TND", "ت.د", N_("Tunisian Dinar")},
83 {"TRY", "TL", N_("New Turkish Lira")},
84 {"TTD", "$", N_("T&T Dollar (TTD)")},
85 {"USD", "$", N_("US Dollar")},
86 {"UYU", "$", N_("Uruguayan Peso")},
87 {"VEF", "Bs F", N_("Venezuelan Bolívar")},
88 {"ZAR", "R", N_("South African Rand")},
89 {NULL, NULL}
92 static gboolean downloading_imf_rates = FALSE, downloading_ecb_rates = FALSE;
93 static gboolean loaded_rates = FALSE;
94 static gboolean load_rates(CurrencyManager *manager);
96 struct CurrencyManagerPrivate
98 GList *currencies;
101 G_DEFINE_TYPE (CurrencyManager, currency_manager, G_TYPE_OBJECT);
104 enum {
105 UPDATED,
106 LAST_SIGNAL
108 static guint signals[LAST_SIGNAL] = { 0, };
110 static CurrencyManager *default_currency_manager = NULL;
113 CurrencyManager *
114 currency_manager_get_default(void)
116 int i;
118 if (default_currency_manager)
119 return default_currency_manager;
121 default_currency_manager = g_object_new(currency_manager_get_type(), NULL);
123 for (i = 0; currency_info[i].short_name; i++) {
124 Currency *c = currency_new(currency_info[i].short_name,
125 _(currency_info[i].long_name),
126 currency_info[i].symbol);
127 default_currency_manager->priv->currencies = g_list_append(default_currency_manager->priv->currencies, c);
130 return default_currency_manager;
134 GList *
135 currency_manager_get_currencies(CurrencyManager *manager)
137 g_return_val_if_fail(manager != NULL, NULL);
138 return manager->priv->currencies;
142 Currency *
143 currency_manager_get_currency(CurrencyManager *manager, const gchar *name)
145 g_return_val_if_fail(manager != NULL, NULL);
146 g_return_val_if_fail(name != NULL, NULL);
148 GList *link;
149 for (link = manager->priv->currencies; link; link = link->next) {
150 Currency *c = link->data;
151 const MPNumber *value;
153 value = currency_get_value(c);
155 if (!strcmp(name, currency_get_name(c))) {
156 if (mp_is_negative(value) ||
157 mp_is_zero(value)) {
158 return NULL;
160 else
161 return c;
164 return NULL;
168 static char *
169 get_imf_rate_filepath()
171 return g_build_filename(g_get_user_cache_dir (),
172 "gcalctool",
173 "rms_five.xls",
174 NULL);
178 static char *
179 get_ecb_rate_filepath()
181 return g_build_filename(g_get_user_cache_dir (),
182 "gcalctool",
183 "eurofxref-daily.xml",
184 NULL);
188 static Currency *
189 add_currency(CurrencyManager *manager, const gchar *short_name)
191 GList *iter;
192 Currency *c;
194 for (iter = manager->priv->currencies; iter; iter = iter->next) {
195 c = iter->data;
196 if (strcmp(short_name, currency_get_name(c)) == 0)
197 return c;
200 g_warning("Currency %s is not in the currency table", short_name);
201 c = currency_new(short_name, short_name, short_name);
202 manager->priv->currencies = g_list_append(manager->priv->currencies, c);
204 return c;
208 /* A file needs to be redownloaded if it doesn't exist, or is too old.
209 * When an error occur, it probably won't hurt to try to download again.
211 static gboolean
212 file_needs_update(gchar *filename, double max_age)
214 struct stat buf;
216 if (!g_file_test(filename, G_FILE_TEST_IS_REGULAR))
217 return TRUE;
219 if (g_stat(filename, &buf) == -1)
220 return TRUE;
222 if (difftime(time(NULL), buf.st_mtime) > max_age)
223 return TRUE;
225 return FALSE;
229 static void
230 download_imf_cb(GObject *object, GAsyncResult *result, gpointer user_data)
232 CurrencyManager *manager = user_data;
233 GError *error = NULL;
235 if (g_file_copy_finish(G_FILE(object), result, &error))
236 g_debug("IMF rates updated");
237 else
238 g_warning("Couldn't download IMF currency rate file: %s", error->message);
239 g_clear_error(&error);
240 downloading_imf_rates = FALSE;
241 load_rates(manager);
245 static void
246 download_ecb_cb(GObject *object, GAsyncResult *result, gpointer user_data)
248 CurrencyManager *manager = user_data;
249 GError *error = NULL;
251 if (g_file_copy_finish(G_FILE(object), result, &error))
252 g_debug("ECB rates updated");
253 else
254 g_warning("Couldn't download ECB currency rate file: %s", error->message);
255 g_clear_error(&error);
256 downloading_ecb_rates = FALSE;
257 load_rates(manager);
261 static void
262 download_file(CurrencyManager *manager, gchar *uri, gchar *filename, GAsyncReadyCallback callback)
264 gchar *directory;
265 GFile *source, *dest;
267 directory = g_path_get_dirname(filename);
268 g_mkdir_with_parents(directory, 0755);
269 g_free(directory);
271 source = g_file_new_for_uri(uri);
272 dest = g_file_new_for_path(filename);
274 g_file_copy_async(source, dest, G_FILE_COPY_OVERWRITE, G_PRIORITY_DEFAULT, NULL, NULL, NULL, callback, manager);
275 g_object_unref(source);
276 g_object_unref(dest);
280 static void
281 load_imf_rates(CurrencyManager *manager)
283 gchar *filename;
284 gchar *data, **lines;
285 gsize length;
286 GError *error = NULL;
287 int i;
288 gboolean result, in_data = FALSE;
289 struct
291 const gchar *name, *symbol;
292 } name_map[] =
294 {"Euro", "EUR"},
295 {"Japanese Yen", "JPY"},
296 {"U.K. Pound Sterling", "GBP"},
297 {"U.S. Dollar", "USD"},
298 {"Algerian Dinar", "DZD"},
299 {"Australian Dollar", "AUD"},
300 {"Bahrain Dinar", "BHD"},
301 {"Botswana Pula", "BWP"},
302 {"Brazilian Real", "BRL"},
303 {"Brunei Dollar", "BND"},
304 {"Canadian Dollar", "CAD"},
305 {"Chilean Peso", "CLP"},
306 {"Chinese Yuan", "CNY"},
307 {"Colombian Peso", "COP"},
308 {"Czech Koruna", "CZK"},
309 {"Danish Krone", "DKK"},
310 {"Hungarian Forint", "HUF"},
311 {"Icelandic Krona", "ISK"},
312 {"Indian Rupee", "INR"},
313 {"Indonesian Rupiah", "IDR"},
314 {"Iranian Rial", "IRR"},
315 {"Israeli New Sheqel", "ILS"},
316 {"Kazakhstani Tenge", "KZT"},
317 {"Korean Won", "KRW"},
318 {"Kuwaiti Dinar", "KWD"},
319 {"Libyan Dinar", "LYD"},
320 {"Malaysian Ringgit", "MYR"},
321 {"Mauritian Rupee", "MUR"},
322 {"Mexican Peso", "MXN"},
323 {"Nepalese Rupee", "NPR"},
324 {"New Zealand Dollar", "NZD"},
325 {"Norwegian Krone", "NOK"},
326 {"Rial Omani", "OMR"},
327 {"Pakistani Rupee", "PKR"},
328 {"Nuevo Sol", "PEN"},
329 {"Philippine Peso", "PHP"},
330 {"Polish Zloty", "PLN"},
331 {"Qatar Riyal", "QAR"},
332 {"Russian Ruble", "RUB"},
333 {"Saudi Arabian Riyal", "SAR"},
334 {"Singapore Dollar", "SGD"},
335 {"South African Rand", "ZAR"},
336 {"Sri Lanka Rupee", "LKR"},
337 {"Swedish Krona", "SEK"},
338 {"Swiss Franc", "CHF"},
339 {"Thai Baht", "THB"},
340 {"Trinidad And Tobago Dollar", "TTD"},
341 {"Tunisian Dinar", "TND"},
342 {"U.A.E. Dirham", "AED"},
343 {"Peso Uruguayo", "UYU"},
344 {"Bolivar Fuerte", "VEF"},
345 {NULL, NULL}
348 filename = get_imf_rate_filepath();
349 result = g_file_get_contents(filename, &data, &length, &error);
350 g_free(filename);
351 if (!result)
353 g_warning("Failed to read exchange rates: %s", error->message);
354 g_clear_error(&error);
355 return;
358 lines = g_strsplit(data, "\n", 0);
359 g_free(data);
361 for (i = 0; lines[i]; i++) {
362 gchar *line, **tokens;
364 line = g_strchug(lines[i]);
366 /* Start after first blank line, stop on next */
367 if (line[0] == '\0') {
368 if (!in_data) {
369 in_data = TRUE;
370 continue;
372 else
373 break;
375 if (!in_data)
376 continue;
378 tokens = g_strsplit(line, "\t", 0);
379 if (strcmp(tokens[0], "Currency") != 0) {
380 gint value_index, name_index;
382 for (value_index = 1; tokens[value_index]; value_index++) {
383 gchar *value = g_strchug (tokens[value_index]);
384 if (value[0] != '\0')
385 break;
387 if (tokens[value_index]) {
388 for (name_index = 0; name_map[name_index].name; name_index++) {
389 if (strcmp(name_map[name_index].name, tokens[0]) == 0)
390 break;
392 if (name_map[name_index].name) {
393 Currency *c = currency_manager_get_currency(manager, name_map[name_index].symbol);
394 MPNumber value;
396 if (!c) {
397 g_debug ("Using IMF rate of %s for %s", tokens[value_index], name_map[name_index].symbol);
398 c = add_currency(manager, name_map[name_index].symbol);
400 mp_set_from_string(tokens[value_index], 10, &value);
401 mp_reciprocal(&value, &value);
402 currency_set_value(c, &value);
404 else
405 g_warning("Unknown currency '%s'", tokens[0]);
408 g_strfreev(tokens);
410 g_strfreev(lines);
414 static void
415 set_ecb_rate(CurrencyManager *manager, xmlNodePtr node, Currency *eur_rate)
417 xmlAttrPtr attribute;
418 gchar *name = NULL, *value = NULL;
420 for (attribute = node->properties; attribute; attribute = attribute->next) {
421 if (strcmp((char *)attribute->name, "currency") == 0) {
422 if (name)
423 xmlFree(name);
424 name = (gchar *)xmlNodeGetContent((xmlNodePtr)attribute);
425 } else if (strcmp ((char *)attribute->name, "rate") == 0) {
426 if (value)
427 xmlFree(value);
428 value = (gchar *)xmlNodeGetContent((xmlNodePtr)attribute);
432 /* Use data if value and no rate currently defined */
433 if (name && value && !currency_manager_get_currency(manager, name)) {
434 Currency *c;
435 MPNumber r, v;
437 g_debug ("Using ECB rate of %s for %s", value, name);
438 c = add_currency(manager, name);
439 mp_set_from_string(value, 10, &r);
440 mp_set_from_mp(currency_get_value(eur_rate), &v);
441 mp_multiply(&v, &r, &v);
442 currency_set_value(c, &v);
445 if (name)
446 xmlFree(name);
447 if (value)
448 xmlFree(value);
452 static void
453 set_ecb_fixed_rate(CurrencyManager *manager, const gchar *name, const gchar *value, Currency *eur_rate)
455 Currency *c;
456 MPNumber r, v;
458 g_debug ("Using ECB fixed rate of %s for %s", value, name);
459 c = add_currency(manager, name);
460 mp_set_from_string(value, 10, &r);
461 mp_set_from_mp(currency_get_value(eur_rate), &v);
462 mp_divide(&v, &r, &v);
463 currency_set_value(c, &v);
467 static void
468 load_ecb_rates(CurrencyManager *manager)
470 Currency *eur_rate;
471 char *filename = get_ecb_rate_filepath();
472 xmlDocPtr document;
473 xmlXPathContextPtr xpath_ctx;
474 xmlXPathObjectPtr xpath_obj;
475 int i, len;
477 /* Scale rates to the EUR value */
478 eur_rate = currency_manager_get_currency(manager, "EUR");
479 if (!eur_rate) {
480 g_warning("Cannot use ECB rates as don't have EUR rate");
481 return;
484 /* Set some fixed rates */
485 set_ecb_fixed_rate(manager, "EEK", "0.06391", eur_rate);
487 xmlInitParser();
488 document = xmlReadFile(filename, NULL, 0);
489 g_free (filename);
490 if (document == NULL) {
491 g_error("Couldn't parse ECB rate file %s", filename);
492 return;
495 xpath_ctx = xmlXPathNewContext(document);
496 if (xpath_ctx == NULL) {
497 xmlFreeDoc(document);
498 g_error("Couldn't create XPath context");
499 return;
502 xmlXPathRegisterNs(xpath_ctx,
503 BAD_CAST("xref"),
504 BAD_CAST("http://www.ecb.int/vocabulary/2002-08-01/eurofxref"));
505 xpath_obj = xmlXPathEvalExpression(BAD_CAST("//xref:Cube[@currency][@rate]"),
506 xpath_ctx);
507 if (xpath_obj == NULL) {
508 xmlXPathFreeContext(xpath_ctx);
509 xmlFreeDoc(document);
510 fprintf(stderr, "Couldn't create XPath object\n");
511 return;
513 len = (xpath_obj->nodesetval) ? xpath_obj->nodesetval->nodeNr : 0;
514 for (i = 0; i < len; i++) {
515 if (xpath_obj->nodesetval->nodeTab[i]->type == XML_ELEMENT_NODE)
516 set_ecb_rate(manager, xpath_obj->nodesetval->nodeTab[i], eur_rate);
518 /* Avoid accessing removed elements */
519 if (xpath_obj->nodesetval->nodeTab[i]->type != XML_NAMESPACE_DECL)
520 xpath_obj->nodesetval->nodeTab[i] = NULL;
523 xmlXPathFreeObject(xpath_obj);
524 xmlXPathFreeContext(xpath_ctx);
525 xmlFreeDoc(document);
526 xmlCleanupParser();
530 static gboolean
531 load_rates(CurrencyManager *manager)
533 int i;
535 /* Already loaded */
536 if (loaded_rates)
537 return TRUE;
539 /* In process */
540 if (downloading_imf_rates || downloading_ecb_rates)
541 return FALSE;
543 /* Use the IMF provided values and top up with currencies tracked by the ECB and not the IMF */
544 load_imf_rates(manager);
545 load_ecb_rates(manager);
547 for (i = 0; currency_info[i].short_name; i++) {
548 GList *link;
549 for (link = manager->priv->currencies; link; link = link->next) {
550 Currency *c = link->data;
551 if (strcmp(currency_get_name(c), currency_info[i].short_name) == 0)
552 break;
554 if (!link)
555 g_warning("Currency %s is not provided by IMF or ECB", currency_info[i].short_name);
558 g_debug("Rates loaded");
559 loaded_rates = TRUE;
561 g_signal_emit(manager, signals[UPDATED], 0);
563 return TRUE;
567 const MPNumber *
568 currency_manager_get_value(CurrencyManager *manager, const gchar *currency)
570 gchar *path;
571 Currency *c;
573 g_return_val_if_fail(manager != NULL, NULL);
574 g_return_val_if_fail(currency != NULL, NULL);
576 /* Update rates if necessary */
577 path = get_imf_rate_filepath();
578 if (!downloading_imf_rates && file_needs_update(path, 60 * 60 * 24 * 7)) {
579 downloading_imf_rates = TRUE;
580 g_debug("Downloading rates from the IMF...");
581 download_file(manager, "http://www.imf.org/external/np/fin/data/rms_five.aspx?tsvflag=Y", path, download_imf_cb);
583 g_free(path);
584 path = get_ecb_rate_filepath();
585 if (!downloading_ecb_rates && file_needs_update(path, 60 * 60 * 24 * 7)) {
586 downloading_ecb_rates = TRUE;
587 g_debug("Downloading rates from the ECB...");
588 download_file(manager, "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml", path, download_ecb_cb);
590 g_free(path);
592 if (!load_rates(manager))
593 return NULL;
595 c = currency_manager_get_currency(manager, currency);
596 if (c)
597 return currency_get_value(c);
598 else
599 return NULL;
603 static void
604 currency_manager_class_init(CurrencyManagerClass *klass)
606 g_type_class_add_private(klass, sizeof(CurrencyManagerPrivate));
608 signals[UPDATED] =
609 g_signal_new("updated",
610 G_TYPE_FROM_CLASS (klass),
611 G_SIGNAL_RUN_LAST,
612 G_STRUCT_OFFSET (CurrencyManagerClass, updated),
613 NULL, NULL,
614 g_cclosure_marshal_VOID__VOID,
615 G_TYPE_NONE, 0);
619 static void
620 currency_manager_init(CurrencyManager *manager)
622 manager->priv = G_TYPE_INSTANCE_GET_PRIVATE(manager, currency_manager_get_type(), CurrencyManagerPrivate);