astrognome/src/ag-db.c

1104 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <gio/gio.h>
#include <gobject/gobject.h>
#include <libgda/libgda.h>
#include <sql-parser/gda-sql-parser.h>
#include <gobject/gvaluecollector.h>
#include "config.h"
#include "ag-app.h"
#include "ag-db.h"
#define SCHEMA_VERSION 1
static AgDb *singleton = NULL;
typedef struct _AgDbPrivate {
gchar *dsn;
GdaConnection *conn;
} AgDbPrivate;
G_DEFINE_QUARK(ag_db_error_quark, ag_db_error);
G_DEFINE_TYPE_WITH_PRIVATE(AgDb, ag_db, G_TYPE_OBJECT);
typedef enum {
COLUMN_ID,
COLUMN_NAME,
COLUMN_COUNTRY,
COLUMN_CITY,
COLUMN_LONGITUDE,
COLUMN_LATITUDE,
COLUMN_ALTITUDE,
COLUMN_YEAR,
COLUMN_MONTH,
COLUMN_DAY,
COLUMN_HOUR,
COLUMN_MINUTE,
COLUMN_SECOND,
COLUMN_TIMEZONE,
COLUMN_HOUSE_SYSTEM,
COLUMN_NOTE,
/* Leave this as the last element */
COLUMN_COUNT
} ChartTableColumn;
typedef struct {
ChartTableColumn id;
gchar *name;
} ChartTableColumnDef;
static ChartTableColumnDef chart_table_column[] = {
{ COLUMN_ID, "id" },
{ COLUMN_NAME, "name" },
{ COLUMN_COUNTRY, "country_name" },
{ COLUMN_CITY, "city_name" },
{ COLUMN_LONGITUDE, "longitude" },
{ COLUMN_LATITUDE, "latitude" },
{ COLUMN_ALTITUDE, "altitude" },
{ COLUMN_YEAR, "year" },
{ COLUMN_MONTH, "month" },
{ COLUMN_DAY, "day" },
{ COLUMN_HOUR, "hour" },
{ COLUMN_MINUTE, "minute" },
{ COLUMN_SECOND, "second" },
{ COLUMN_TIMEZONE, "timezone" },
{ COLUMN_HOUSE_SYSTEM, "house_system" },
{ COLUMN_NOTE, "note" },
};
/**
* ag_db_non_select:
* @db: the AgDb to operate on
* @sql: the SQL query to execute
*
* Executes a non-SELECT query on @db. No result is returned right now (TODO)
*/
static void
ag_db_non_select(AgDb *db, const gchar *sql)
{
GdaStatement *sth;
gint nrows;
const gchar *remain;
GdaSqlParser *parser;
AgDbPrivate *priv = ag_db_get_instance_private(db);
GError *err = NULL;
parser = g_object_get_data(G_OBJECT(priv->conn), "parser");
g_assert(GDA_IS_SQL_PARSER(parser));
if ((sth = gda_sql_parser_parse_string(
parser,
sql,
&remain,
&err
)) == NULL) {
g_error(
"SQL error: %s",
(err && err->message)
? err->message
: "no reason"
);
}
if ((nrows = gda_connection_statement_execute_non_select(
priv->conn,
sth,
NULL,
NULL,
&err
)) == -1) {
g_error(
"SQL error: %s",
(err && err->message)
? err->message
: "no details"
);
}
g_object_unref(sth);
}
/**
* ag_db_table_exists:
* @db: the #AgDb object to operate on
* @table: the nawe of the table to check for
*
* Checks if the specified table exists. It is done by querying the
* sqlite_master system table.
*
* Returns: TRUE if the table exists, FALSE otherwise
*/
static gboolean
ag_db_table_exists(AgDb *db, const gchar *table)
{
GdaSqlParser *parser;
GdaStatement *sth;
GdaDataModel *result;
GdaSet *params;
GdaHolder *holder;
const gchar *remain;
gboolean ret;
GValue table_value = G_VALUE_INIT;
AgDbPrivate *priv = ag_db_get_instance_private(db);
GError *err = NULL;
parser = g_object_get_data(G_OBJECT(priv->conn), "parser");
if ((sth = gda_sql_parser_parse_string(
parser,
"SELECT type \n" \
" FROM sqlite_master \n" \
" WHERE type = 'table' \n" \
" AND name = ##name::string",
&remain,
&err
)) == NULL) {
g_error(
"SQL error: %s",
(err && err->message)
? err->message
: "no reason"
);
}
if (gda_statement_get_parameters(sth, &params, &err) == FALSE) {
g_error(
"Params error: %s",
(err && err->message)
? err->message
: "no reason"
);
}
holder = gda_set_get_holder(params, "name");
g_value_init(&table_value, G_TYPE_STRING);
g_value_set_string(&table_value, table);
gda_holder_set_value(holder, &table_value, &err);
g_value_unset(&table_value);
result = gda_connection_statement_execute_select(
priv->conn,
sth,
params,
&err
);
if (gda_data_model_get_n_rows(result) > 0) {
ret = TRUE;
} else {
ret = FALSE;
}
g_object_unref(result);
g_object_unref(sth);
return ret;
}
/**
* ag_db_select:
* @db: the database object to work on
* @err: a #GError or NULL
* @sql: the query to execute
* @...: a NULL terminated list of key-value pairs of the query parameters
*
* Returns: (transfer full): the #GdaDataModel as the result of the query
*/
static GdaDataModel *
ag_db_select(AgDb *db, GError **err, const gchar *sql, ...)
{
GdaSqlParser *parser;
const gchar *remain;
GdaSet *params;
GdaStatement *sth;
GdaDataModel *ret;
gchar *error = NULL;
GError *local_err = NULL;
AgDbPrivate *priv = ag_db_get_instance_private(db);
parser = g_object_get_data(G_OBJECT(priv->conn), "parser");
if ((sth = gda_sql_parser_parse_string(
parser,
sql,
&remain,
&local_err
)) == NULL) {
g_error(
"SQL error: %s",
(local_err && local_err->message)
? local_err->message
: "no reason"
);
}
if (!gda_statement_get_parameters(sth, &params, &local_err)) {
g_error(
"SQL error: %s",
(local_err && local_err->message)
? local_err->message
: "no reason"
);
}
if (params) {
va_list ap;
va_start(ap, sql);
while (TRUE) {
gchar *key;
GdaHolder *holder;
GType type;
GValue value = G_VALUE_INIT;
if ((key = va_arg(ap, gchar *)) == NULL) {
break;
}
if ((holder = gda_set_get_holder(
params,
(const gchar *)key
)) == NULL) {
g_error("Error: holder %s is not defined in query.", key);
}
type = gda_holder_get_g_type(holder);
g_value_init(&value, type);
G_VALUE_COLLECT_INIT(&value, type, ap, 0, &error);
if (error) {
g_error("SQL GValue error: %s", error);
}
if (!gda_holder_set_value(holder, (const GValue *)&value, err)) {
g_error(
"SQL GdaHolder error: %s",
(*err && (*err)->message)
? (*err)->message
: "no reason"
);
}
}
va_end(ap);
}
ret = gda_connection_statement_execute_select(priv->conn, sth, params, err);
g_object_unref(sth);
return ret;
}
/**
* ag_db_check_version_table:
* @db: the #AgDb object to operate on
*
* Checks if the version table exists, and creates it if necessary. It doesn't
* check if the structure is valid!
*/
static void
ag_db_check_version_table(AgDb *db)
{
if (ag_db_table_exists(db, "version")) {
GdaDataModel *result;
gint ret;
g_debug(
"Version table exists. " \
"Checking if db_version is %d and app_version is %s",
SCHEMA_VERSION,
PACKAGE_VERSION
);
result = ag_db_select(
db,
NULL,
"SELECT db_version, app_version FROM version",
NULL
);
ret = gda_data_model_get_n_rows(result);
if (ret < 0) {
g_error("No number of rows?");
} else if (ret > 1) {
g_error("Error in database. Version table has more than one rows!");
} else if (ret == 0) {
// TODO: Check schema against current one maybe? If its fine, we
// may just add a row here.
g_error("Error in database. Version table has no rows!");
} else {
// Version table has one row
const GValue *value;
GdaDataModelIter *iter = gda_data_model_create_iter(result);
gint version;
gda_data_model_iter_move_next(iter);
value = gda_data_model_iter_get_value_at(iter, 0);
if (!G_VALUE_HOLDS_INT(value)) {
g_error(
"Database is invalid. " \
"version.db_version should be an integer."
);
}
version = g_value_get_int(value);
if (version < SCHEMA_VERSION) {
// TODO
g_error("Update required!");
} else if (version > SCHEMA_VERSION) {
const GValue *app_version_value;
const gchar *app_version;
app_version_value = gda_data_model_iter_get_value_at(
iter,
1
);
app_version = g_value_get_string(
app_version_value
);
g_error(
"The version of your database is from the future. " \
"It seems it was created by Astrognome v%s.",
app_version
);
} else {
g_object_unref(result);
}
}
} else {
GValue db_version = G_VALUE_INIT,
app_version = G_VALUE_INIT;
AgDbPrivate *priv = ag_db_get_instance_private(db);
g_value_init(&db_version, G_TYPE_INT);
g_value_init(&app_version, G_TYPE_STRING);
g_value_set_int(&db_version, SCHEMA_VERSION);
g_value_set_static_string(&app_version, PACKAGE_VERSION);
ag_db_non_select(
db,
"CREATE TABLE version (" \
"id INTEGER PRIMARY KEY, " \
"db_version INTEGER UNIQUE NOT NULL, " \
"app_version TEXT UNIQUE NOT NULL" \
")"
);
gda_connection_insert_row_into_table(
priv->conn,
"version",
NULL,
"db_version", &db_version,
"app_version", &app_version,
NULL
);
}
}
/**
* ag_db_check_chart_table:
* @db: the #AgDb object to operate on
*
* Checks if the chart table exists, and creates it if necessary. It doesn't
* check if the structure is valid!
*/
static void
ag_db_check_chart_table(AgDb *db)
{
ag_db_non_select(
db,
"CREATE TABLE IF NOT EXISTS chart (" \
"id INTEGER PRIMARY KEY, " \
"name TEXT NOT NULL, " \
"country_name TEXT, " \
"city_name TEXT, " \
"longitude DOUBLE NOT NULL, " \
"latitude DOUBLE NOT NULL, " \
"altitude DOUBLE, " \
"year INTEGER NOT NULL, " \
"month UNSIGNED INTEGER NOT NULL, " \
"day UNSIGNED INTEGER NOT NULL, " \
"hour UNSIGNED INTEGER NOT NULL, " \
"minute UNSIGNED INTEGER NOT NULL, " \
"second UNSIGNED INTEGER NOT NULL, " \
"timezone DOUBLE NOT NULL, " \
"house_system TEXT NOT NULL, " \
"note TEXT" \
")"
);
}
/**
* ag_db_verify:
* @db: the #AgDb object to operate on
*
* Checks if the database file is sane.
*
* Returns: the status of the database (TODO: make this an enum!)
*/
static gint
ag_db_verify(AgDb *db)
{
ag_db_check_version_table(db);
ag_db_check_chart_table(db);
return 0;
}
static void
ag_db_init(AgDb *db)
{
GdaSqlParser *parser;
GFile *user_data_dir = g_file_new_for_path(g_get_user_data_dir()),
*ag_data_dir = g_file_get_child(user_data_dir, "astrognome");
AgDbPrivate *priv = ag_db_get_instance_private(db);
gchar *path = g_file_get_path(ag_data_dir);
GError *err = NULL;
gda_init();
if (!g_file_query_exists(ag_data_dir, NULL)) {
gchar *path = g_file_get_path(ag_data_dir);
if (g_mkdir_with_parents(path, 0700) != 0) {
g_error(
"Data directory %s does not exist and can not be created.",
path
);
}
}
priv->dsn = g_strdup_printf("SQLite://DB_DIR=%s;DB_NAME=charts", path);
g_free(path);
g_object_unref(user_data_dir);
g_object_unref(ag_data_dir);
priv->conn = gda_connection_open_from_string(
NULL,
priv->dsn,
NULL,
GDA_CONNECTION_OPTIONS_NONE,
&err
);
if (priv->conn == NULL) {
g_error(
"Unable to initialize database: %s",
(err && err->message)
? err->message
: "no reason"
);
}
if ((parser = gda_connection_create_parser(priv->conn)) == NULL) {
parser = gda_sql_parser_new();
}
g_object_set_data_full(
G_OBJECT(priv->conn),
"parser",
parser,
g_object_unref
);
ag_db_verify(db);
}
static void
ag_db_dispose(GObject *gobject)
{
AgDbPrivate *priv = ag_db_get_instance_private(AG_DB(gobject));
g_object_unref(priv->conn);
g_free(priv->dsn);
G_OBJECT_CLASS(ag_db_parent_class)->dispose(gobject);
}
static void
ag_db_finalize(GObject *gobject)
{
singleton = NULL;
G_OBJECT_CLASS(ag_db_parent_class)->finalize(gobject);
}
static void
ag_db_class_init(AgDbClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
gobject_class->dispose = ag_db_dispose;
gobject_class->finalize = ag_db_finalize;
}
AgDb *
ag_db_get(void)
{
if (!singleton) {
singleton = AG_DB(g_object_new(AG_TYPE_DB, NULL));
} else {
g_object_ref(singleton);
}
g_assert(singleton);
return singleton;
}
/**
* ag_db_save_data_free:
* @save_data: the #AgDbSave struct to free
*
* Frees @save_data and all its fields
*/
void
ag_db_save_data_free(AgDbSave *save_data)
{
if (!save_data) {
return;
}
if (save_data->name) {
g_free(save_data->name);
}
if (save_data->country) {
g_free(save_data->country);
}
if (save_data->city) {
g_free(save_data->city);
}
if (save_data->house_system) {
g_free(save_data->house_system);
}
if (save_data->note) {
g_free(save_data->note);
}
g_free(save_data);
}
/**
* ag_db_save_chart:
* @db: the #AgDb object to operate on
* @save_data: the data to save.
* @window: the window to use as the parent of possible message dialogs (TODO:
* this is a design flow. This function should merely rely only on
* @err)
* @err: a #GError for storing errors
*
* Saves @save_data to the database. If its db_id field is -1, a new record is
* created; otherwise the row with the given ID will be updated. Should any
* issues arise, a message dialog will pop up on @window
*
* Returns: TRUE if the save succeeds, FALSE otherwise
*/
gboolean
ag_db_save_chart(AgDb *db, AgDbSave *save_data, GtkWidget *window, GError **err)
{
GValue db_id = G_VALUE_INIT,
name = G_VALUE_INIT,
country = G_VALUE_INIT,
city = G_VALUE_INIT,
longitude = G_VALUE_INIT,
latitude = G_VALUE_INIT,
altitude = G_VALUE_INIT,
year = G_VALUE_INIT,
month = G_VALUE_INIT,
day = G_VALUE_INIT,
hour = G_VALUE_INIT,
minute = G_VALUE_INIT,
second = G_VALUE_INIT,
timezone = G_VALUE_INIT,
house_system = G_VALUE_INIT,
note = G_VALUE_INIT;
AgDbPrivate *priv = ag_db_get_instance_private(db);
g_value_init(&name, G_TYPE_STRING);
g_value_set_string(&name, save_data->name);
g_value_init(&country, G_TYPE_STRING);
g_value_set_string(&country, save_data->country);
g_value_init(&city, G_TYPE_STRING);
g_value_set_string(&city, save_data->city);
g_value_init(&longitude, G_TYPE_DOUBLE);
g_value_set_double(&longitude, save_data->longitude);
g_value_init(&latitude, G_TYPE_DOUBLE);
g_value_set_double(&latitude, save_data->latitude);
g_value_init(&altitude, G_TYPE_DOUBLE);
g_value_set_double(&altitude, save_data->altitude);
g_value_init(&year, G_TYPE_INT);
g_value_set_int(&year, save_data->year);
g_value_init(&month, G_TYPE_UINT);
g_value_set_uint(&month, save_data->month);
g_value_init(&day, G_TYPE_UINT);
g_value_set_uint(&day, save_data->day);
g_value_init(&hour, G_TYPE_UINT);
g_value_set_uint(&hour, save_data->hour);
g_value_init(&minute, G_TYPE_UINT);
g_value_set_uint(&minute, save_data->minute);
g_value_init(&second, G_TYPE_UINT);
g_value_set_uint(&second, save_data->second);
g_value_init(&timezone, G_TYPE_DOUBLE);
g_value_set_double(&timezone, save_data->timezone);
g_value_init(&house_system, G_TYPE_STRING);
g_value_set_string(&house_system, save_data->house_system);
g_value_init(&note, G_TYPE_STRING);
g_value_set_string(&note, save_data->note);
if (save_data->db_id == -1) {
if (!gda_connection_insert_row_into_table(
priv->conn,
"chart",
err,
"name", &name,
"country_name", &country,
"city_name", &city,
"longitude", &longitude,
"latitude", &latitude,
"altitude", &altitude,
"year", &year,
"month", &month,
"day", &day,
"hour", &hour,
"minute", &minute,
"second", &second,
"timezone", &timezone,
"house_system", &house_system,
"note", &note,
NULL
)) {
ag_app_message_dialog(
window,
GTK_MESSAGE_ERROR,
"Unable to save: %s",
(*err && (*err)->message)
? (*err)->message
: "no reason"
);
}
} else {
g_value_init(&db_id, G_TYPE_INT);
g_value_set_int(&db_id, save_data->db_id);
if (!gda_connection_update_row_in_table(
priv->conn,
"chart",
"id",
&db_id,
err,
"name", &name,
"country_name", &country,
"city_name", &city,
"longitude", &longitude,
"latitude", &latitude,
"altitude", &altitude,
"year", &year,
"month", &month,
"day", &day,
"hour", &hour,
"minute", &minute,
"second", &second,
"timezone", &timezone,
"house_system", &house_system,
"note", &note,
NULL
)) {
ag_app_message_dialog(
window,
GTK_MESSAGE_ERROR,
"Unable to save: %s",
(*err && (*err)->message)
? (*err)->message
: "no reason"
);
}
g_value_unset(&db_id);
}
g_value_unset(&note);
g_value_unset(&house_system);
g_value_unset(&timezone);
g_value_unset(&second);
g_value_unset(&minute);
g_value_unset(&hour);
g_value_unset(&day);
g_value_unset(&month);
g_value_unset(&year);
g_value_unset(&altitude);
g_value_unset(&latitude);
g_value_unset(&longitude);
g_value_unset(&city);
g_value_unset(&country);
g_value_unset(&name);
return FALSE;
}
/**
* ag_db_get_chart_list:
* @db: the #AgDb object to operate on
* @err: a #GError
*
* Creates a list of all charts in the database, ordered by name. As the return
* value may be NULL even if there are no charts or if there was an error, you
* may want to check @err if the return value is NULL.
*
* Please be aware that the #AgDbSave objects of the returned value are not
* fully realised chart records. To get one, you need to call
* ag_db_get_chart_data_by_id()
*
* Returns: (element-type AgDbSave) (transfer full): the list of all charts, or
* or NULL if there are none, or if there is an error.
*/
GList *
ag_db_get_chart_list(AgDb *db, GError **err)
{
GdaDataModelIter *iter;
GList *ret = NULL;
GdaDataModel *result = ag_db_select(
db,
err,
"SELECT id, name FROM chart ORDER BY name",
NULL
);
if (result == NULL) {
return NULL;
}
iter = gda_data_model_create_iter(result);
while (gda_data_model_iter_move_next(iter)) {
const GValue *value;
AgDbSave *save_data = g_new0(AgDbSave, 1);
value = gda_data_model_iter_get_value_at(iter, 0);
save_data->db_id = g_value_get_int(value);
value = gda_data_model_iter_get_value_at(iter, 1);
save_data->name = g_strdup(g_value_get_string(value));
ret = g_list_prepend(ret, save_data);
}
return g_list_reverse(ret);
}
/**
* ag_db_get_chart_data_by_id:
* @db: the #AgDb object to operate on
* @row_id: the ID field of the requested chart
* @err: a #GError
*
* Fetches the specified row from the chart table.
*
* Returns: (transfer full): A fully filled #AgDbSave record of the chart
*/
AgDbSave *
ag_db_get_chart_data_by_id(AgDb *db, guint row_id, GError **err)
{
AgDbSave *save_data;
const GValue *value;
GdaValueAttribute attributes;
gchar *query,
*columns;
guint i;
GdaDataModel *result;
GError *local_err = NULL;
columns = NULL;
for (i = 1; i < COLUMN_COUNT; i++) {
gchar *tmp;
if (i == 1) {
columns = g_strjoin(
", ",
chart_table_column[0].name,
chart_table_column[1].name,
NULL
);
} else {
tmp = g_strjoin(", ", columns, chart_table_column[i].name, NULL);
g_free(columns);
columns = tmp;
}
}
query = g_strdup_printf(
"SELECT %s FROM chart WHERE id = ##id::gint",
columns
);
g_free(columns);
result = ag_db_select(db, &local_err, query, "id", row_id, NULL);
g_free(query);
if (local_err && (local_err->message)) {
return NULL;
}
if (gda_data_model_get_n_rows(result) < 1) {
g_set_error(
err,
AG_DB_ERROR, AG_DB_ERROR_NO_CHART,
"Chart does not exist"
);
return NULL;
}
save_data = g_new0(AgDbSave, 1);
/* id */
value = gda_data_model_get_value_at(result, COLUMN_ID, 0, NULL);
save_data->db_id = g_value_get_int(value);
/* name */
value = gda_data_model_get_value_at(result, COLUMN_NAME, 0, NULL);
save_data->name = g_strdup(g_value_get_string(value));
/* country */
value = gda_data_model_get_value_at(result, COLUMN_COUNTRY, 0, NULL);
attributes = gda_data_model_get_attributes_at(result, COLUMN_COUNTRY, 0);
if (attributes | GDA_VALUE_ATTR_IS_NULL) {
save_data->country = NULL;
} else {
save_data->country = g_strdup(g_value_get_string(value));
}
value = gda_data_model_get_value_at(result, COLUMN_CITY, 0, NULL);
attributes = gda_data_model_get_attributes_at(result, COLUMN_CITY, 0);
if (attributes | GDA_VALUE_ATTR_IS_NULL) {
save_data->city = NULL;
} else {
save_data->city = g_strdup(g_value_get_string(value));
}
value = gda_data_model_get_value_at(
result,
COLUMN_LONGITUDE,
0,
NULL
);
save_data->longitude = g_value_get_double(value);
value = gda_data_model_get_value_at(
result,
COLUMN_LATITUDE,
0,
NULL
);
save_data->latitude = g_value_get_double(value);
value = gda_data_model_get_value_at(result, COLUMN_ALTITUDE, 0, NULL);
attributes = gda_data_model_get_attributes_at(result, COLUMN_ALTITUDE, 0);
if (attributes | GDA_VALUE_ATTR_IS_NULL) {
/* TODO: this value should be macroified */
save_data->altitude = 280.0;
} else {
save_data->altitude = g_value_get_double(value);
}
value = gda_data_model_get_value_at(result, COLUMN_YEAR, 0, NULL);
save_data->year = g_value_get_int(value);
value = gda_data_model_get_value_at(
result,
COLUMN_MONTH,
0,
NULL
);
save_data->month = g_value_get_uint(value);
value = gda_data_model_get_value_at(result, COLUMN_DAY, 0, NULL);
save_data->day = g_value_get_uint(value);
value = gda_data_model_get_value_at(result, COLUMN_HOUR, 0, NULL);
save_data->hour = g_value_get_uint(value);
value = gda_data_model_get_value_at(
result,
COLUMN_MINUTE,
0,
NULL
);
save_data->minute = g_value_get_uint(value);
value = gda_data_model_get_value_at(
result,
COLUMN_SECOND,
0,
NULL
);
save_data->second = g_value_get_uint(value);
value = gda_data_model_get_value_at(
result,
COLUMN_TIMEZONE,
0,
NULL
);
save_data->timezone = g_value_get_double(value);
value = gda_data_model_get_value_at(
result,
COLUMN_HOUSE_SYSTEM,
0,
NULL
);
save_data->house_system = g_strdup(g_value_get_string(value));
value = gda_data_model_get_value_at(result, COLUMN_NOTE, 0, NULL);
attributes = gda_data_model_get_attributes_at(result, 15, 0);
if (attributes | GDA_VALUE_ATTR_IS_NULL) {
save_data->note = NULL;
} else {
save_data->note = g_strdup(g_value_get_string(value));
}
g_object_unref(result);
return save_data;
}
/**
* string_collate:
* @str1: the first string
* @str2: the second string
*
* A wrapper function around g_utf8_collate() that can handle NULL values. NULL
* precedes any strings (even "").
*
* Returns: -1 if str1 is ordered before str2, 1 if str2 comes first, or 0 if
* they are identical
*/
static gint
string_collate(const gchar *str1, const gchar *str2)
{
if (((str1 == NULL) || (str2 == NULL)) && (str1 != str2)) {
return (str1 == NULL) ? -1 : 1;
}
if (str1 == str2) {
return 0;
}
return g_utf8_collate(str1, str2);
}
/**
* ag_db_save_identical:
* @a: the first #AgDbSave structure
* @b: the second #AgDbSave structure
*
* Compares two #AgDbSave structures and their contents.
*
* Returns: TRUE if the two structs hold equal values (strings are also compared
* with string_collate()), FALSE otherwise
*/
gboolean
ag_db_save_identical(const AgDbSave *a, const AgDbSave *b)
{
if (a == b) {
return TRUE;
}
if ((a == NULL) || (b == NULL)) {
return FALSE;
}
if (string_collate(a->name, b->name) != 0) {
return FALSE;
}
if (string_collate(a->country, b->country) != 0) {
return FALSE;
}
if (string_collate(a->city, b->city) != 0) {
return FALSE;
}
if (a->longitude != b->longitude) {
return FALSE;
}
if (a->latitude != b->latitude) {
return FALSE;
}
if (a->altitude != b->altitude) {
return FALSE;
}
if (a->year != b->year) {
return FALSE;
}
if (a->month != b->month) {
return FALSE;
}
if (a->day != b->day) {
return FALSE;
}
if (a->hour != b->hour) {
return FALSE;
}
if (a->minute != b->minute) {
return FALSE;
}
if (a->second != b->second) {
return FALSE;
}
if (a->timezone != b->timezone) {
return FALSE;
}
if (string_collate(a->house_system, b->house_system) != 0) {
return FALSE;
}
if (string_collate(a->note, b->note) != 0) {
return FALSE;
}
return TRUE;
}