/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* e-card-compare.c
*
* Copyright (C) 2001 Ximian, Inc.
*
* Developed by Jon Trowbridge <trow@ximian.com>
*/
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA.
*/
#include <config.h>
#include <ctype.h>
#include <gal/unicode/gunicode.h>
#include "e-book-util.h"
#include "e-card-compare.h"
/* This is an "optimistic" combiner: the best of the two outcomes is
selected. */
static ECardMatchType
combine_comparisons (ECardMatchType prev,
ECardMatchType new_info)
{
if (new_info == E_CARD_MATCH_NOT_APPLICABLE)
return prev;
return (ECardMatchType) MAX ((gint) prev, (gint) new_info);
}
/*** Name comparisons ***/
/* This *so* doesn't belong here... at least not implemented in a
sucky way like this. But by getting it in here now, I can fix it
up w/o adding a new feature when we are in feature freeze. :-) */
/* This is very Anglocentric. */
static gchar *name_synonyms[][2] = {
{ "jon", "john" }, /* Ah, the hacker's perogative */
{ "joseph", "joe" },
{ "robert", "bob" },
{ "richard", "dick" },
{ "william", "bill" },
{ "anthony", "tony" },
{ "michael", "mike" },
{ "eric", "erik" },
{ "elizabeth", "liz" },
{ "jeff", "geoff" },
{ "jeff", "geoffrey" },
{ "jim", "james" },
{ "abigal", "abby" },
{ "amanda", "amy" },
{ "amanda", "manda" },
{ "jennifer", "jenny" },
{ "rebecca", "becca" },
{ "rebecca", "becky" },
{ "anderson", "andersen" },
/* We could go on and on... */
{ NULL, NULL }
};
static gboolean
name_fragment_match (const gchar *a, const gchar *b)
{
gint i, len_a, len_b;
/* This will cause "Chris" and "Christopher" to match. */
len_a = g_utf8_strlen (a, -1);
len_b = g_utf8_strlen (b, -1);
if (!g_utf8_strncasecmp (a, b, MIN (len_a, len_b)))
return TRUE;
/* Check for nicknames. Yes, the linear search blows. */
for (i=0; name_synonyms[i][0]; ++i) {
if (!g_utf8_strcasecmp (name_synonyms[i][0], a)
&& !g_utf8_strcasecmp (name_synonyms[i][1], b))
return TRUE;
if (!g_utf8_strcasecmp (name_synonyms[i][0], b)
&& !g_utf8_strcasecmp (name_synonyms[i][1], a))
return TRUE;
}
return FALSE;
}
ECardMatchType
e_card_compare_name (ECard *card1, ECard *card2)
{
ECardName *a, *b;
gint matches=0, possible=0;
gboolean given_match = FALSE, additional_match = FALSE, family_match = FALSE;
g_return_val_if_fail (card1 && E_IS_CARD (card1), E_CARD_MATCH_NOT_APPLICABLE);
g_return_val_if_fail (card2 && E_IS_CARD (card2), E_CARD_MATCH_NOT_APPLICABLE);
a = card1->name;
b = card2->name;
if (a == NULL || b == NULL)
return E_CARD_MATCH_NOT_APPLICABLE;
if (a->given && b->given) {
++possible;
if (name_fragment_match (a->given, b->given)) {
++matches;
given_match = TRUE;
}
}
if (a->additional && b->additional) {
++possible;
if (name_fragment_match (a->additional, b->additional)) {
++matches;
additional_match = TRUE;
}
}
if (a->family && b->family) {
++possible;
if (name_fragment_match (a->family, b->family)) {
++matches;
family_match = TRUE;
}
}
/* Now look at the # of matches and try to intelligently map
an E_CARD_MATCH_* type to it. Special consideration is given
to family-name matches. */
if (possible == 0)
return E_CARD_MATCH_NOT_APPLICABLE;
if (possible == 1)
return family_match ? E_CARD_MATCH_VAGUE : E_CARD_MATCH_NONE;
if (possible == matches)
return family_match ? E_CARD_MATCH_EXACT : E_CARD_MATCH_PARTIAL;
if (possible == matches+1)
return family_match ? E_CARD_MATCH_PARTIAL : E_CARD_MATCH_VAGUE;
return E_CARD_MATCH_NONE;
}
/*** Nickname Comparisons ***/
ECardMatchType
e_card_compare_nickname (ECard *card1, ECard *card2)
{
g_return_val_if_fail (card1 && E_IS_CARD (card1), E_CARD_MATCH_NOT_APPLICABLE);
g_return_val_if_fail (card2 && E_IS_CARD (card2), E_CARD_MATCH_NOT_APPLICABLE);
return E_CARD_MATCH_NOT_APPLICABLE;
}
/*** E-mail Comparisons ***/
static gboolean
match_email_username (const gchar *addr1, const gchar *addr2)
{
gint c1, c2;
if (addr1 == NULL || addr2 == NULL)
return FALSE;
while (*addr1 && *addr2 && *addr1 != '@' && *addr2 != '@') {
c1 = isupper (*addr1) ? tolower (*addr1) : *addr1;
c2 = isupper (*addr2) ? tolower (*addr2) : *addr2;
if (c1 != c2)
return FALSE;
++addr1;
++addr2;
}
return *addr1 == *addr2;
}
static gboolean
match_email_hostname (const gchar *addr1, const gchar *addr2)
{
gint c1, c2;
gboolean seen_at1, seen_at2;
if (addr1 == NULL || addr2 == NULL)
return FALSE;
/* Walk to the end of each string. */
seen_at1 = FALSE;
if (*addr1) {
while (*addr1) {
if (*addr1 == '@')
seen_at1 = TRUE;
++addr1;
}
--addr1;
}
seen_at2 = FALSE;
if (*addr2) {
while (*addr2) {
if (*addr2 == '@')
seen_at2 = TRUE;
++addr2;
}
--addr2;
}
if (!seen_at1 && !seen_at2)
return TRUE;
if (!seen_at1 || !seen_at2)
return FALSE;
while (*addr1 != '@' && *addr2 != '@') {
c1 = isupper (*addr1) ? tolower (*addr1) : *addr1;
c2 = isupper (*addr2) ? tolower (*addr2) : *addr2;
if (c1 != c2)
return FALSE;
--addr1;
--addr2;
}
/* This will match bob@foo.ximian.com and bob@ximian.com */
return *addr1 == '.' || *addr2 == '.';
}
static ECardMatchType
compare_email_addresses (const gchar *addr1, const gchar *addr2)
{
if (addr1 == NULL || addr2 == NULL)
return E_CARD_MATCH_NOT_APPLICABLE;
if (match_email_username (addr1, addr2))
return match_email_hostname (addr1, addr2) ? E_CARD_MATCH_EXACT : E_CARD_MATCH_PARTIAL;
return E_CARD_MATCH_NONE;
}
ECardMatchType
e_card_compare_email (ECard *card1, ECard *card2)
{
EIterator *i1, *i2;
ECardMatchType match = E_CARD_MATCH_NOT_APPLICABLE;
g_return_val_if_fail (card1 && E_IS_CARD (card1), E_CARD_MATCH_NOT_APPLICABLE);
g_return_val_if_fail (card2 && E_IS_CARD (card2), E_CARD_MATCH_NOT_APPLICABLE);
if (card1->email == NULL || card2->email == NULL)
return E_CARD_MATCH_NOT_APPLICABLE;
i1 = e_list_get_iterator (card1->email);
i2 = e_list_get_iterator (card2->email);
/* Do pairwise-comparisons on all of the e-mail addresses. If
we find an exact match, there is no reason to keep
checking. */
e_iterator_reset (i1);
while (e_iterator_is_valid (i1) && match != E_CARD_MATCH_EXACT) {
const gchar *addr1 = (const gchar *) e_iterator_get (i1);
e_iterator_reset (i2);
while (e_iterator_is_valid (i2) && match != E_CARD_MATCH_EXACT) {
const gchar *addr2 = (const gchar *) e_iterator_get (i2);
match = combine_comparisons (match, compare_email_addresses (addr1, addr2));
e_iterator_next (i2);
}
e_iterator_next (i1);
}
gtk_object_unref (GTK_OBJECT (i1));
gtk_object_unref (GTK_OBJECT (i2));
return match;
}
ECardMatchType
e_card_compare_address (ECard *card1, ECard *card2)
{
g_return_val_if_fail (card1 && E_IS_CARD (card1), E_CARD_MATCH_NOT_APPLICABLE);
g_return_val_if_fail (card2 && E_IS_CARD (card2), E_CARD_MATCH_NOT_APPLICABLE);
/* Unimplemented */
return E_CARD_MATCH_NOT_APPLICABLE;
}
ECardMatchType
e_card_compare_telephone (ECard *card1, ECard *card2)
{
g_return_val_if_fail (card1 && E_IS_CARD (card1), E_CARD_MATCH_NOT_APPLICABLE);
g_return_val_if_fail (card2 && E_IS_CARD (card2), E_CARD_MATCH_NOT_APPLICABLE);
/* Unimplemented */
return E_CARD_MATCH_NOT_APPLICABLE;
}
ECardMatchType
e_card_compare (ECard *card1, ECard *card2)
{
ECardMatchType result;
g_return_val_if_fail (card1 && E_IS_CARD (card1), E_CARD_MATCH_NOT_APPLICABLE);
g_return_val_if_fail (card2 && E_IS_CARD (card2), E_CARD_MATCH_NOT_APPLICABLE);
result = E_CARD_MATCH_NONE;
result = combine_comparisons (result, e_card_compare_name (card1, card2));
result = combine_comparisons (result, e_card_compare_nickname (card1, card2));
result = combine_comparisons (result, e_card_compare_email (card1, card2));
result = combine_comparisons (result, e_card_compare_address (card1, card2));
result = combine_comparisons (result, e_card_compare_telephone (card1, card2));
return result;
}
typedef struct _MatchSearchInfo MatchSearchInfo;
struct _MatchSearchInfo {
ECard *card;
GList *avoid;
ECardMatchQueryCallback cb;
gpointer closure;
};
static void
match_search_info_free (MatchSearchInfo *info)
{
if (info) {
gtk_object_unref (GTK_OBJECT (info->card));
/* This should already have been deallocated, but just in case... */
if (info->avoid) {
g_list_foreach (info->avoid, (GFunc) gtk_object_unref, NULL);
g_list_free (info->avoid);
info->avoid = NULL;
}
g_free (info);
}
}
static void
simple_query_cb (EBook *book, EBookSimpleQueryStatus status, const GList *cards, gpointer closure)
{
MatchSearchInfo *info = (MatchSearchInfo *) closure;
ECardMatchType best_match = E_CARD_MATCH_NONE;
ECard *best_card = NULL;
GList *remaining_cards = NULL;
const GList *i;
if (status != E_BOOK_SIMPLE_QUERY_STATUS_SUCCESS) {
info->cb (info->card, NULL, E_CARD_MATCH_NONE, info->closure);
match_search_info_free (info);
return;
}
/* remove the cards we're to avoid from the list, if they're present */
for (i = cards; i != NULL; i = g_list_next (i)) {
ECard *this_card = E_CARD (i->data);
GList *iterator;
gboolean avoid = FALSE;
for (iterator = info->avoid; iterator; iterator = iterator->next) {
if (!strcmp (e_card_get_id (iterator->data), e_card_get_id (this_card))) {
avoid = TRUE;
break;
}
}
if (!avoid)
remaining_cards = g_list_prepend (remaining_cards, this_card);
}
remaining_cards = g_list_reverse (remaining_cards);
for (i = remaining_cards; i != NULL; i = g_list_next (i)) {
ECard *this_card = E_CARD (i->data);
ECardMatchType this_match = e_card_compare (info->card, this_card);
if ((gint)this_match > (gint)best_match) {
best_match = this_match;
best_card = this_card;
}
}
g_list_free (remaining_cards);
info->cb (info->card, best_card, best_match, info->closure);
match_search_info_free (info);
}
#define MAX_QUERY_PARTS 10
static void
use_common_book_cb (EBook *book, gpointer closure)
{
MatchSearchInfo *info = (MatchSearchInfo *) closure;
ECard *card = info->card;
gchar *query_parts[MAX_QUERY_PARTS];
gint p=0;
gchar *query, *qj;
int i;
if (book == NULL) {
info->cb (info->card, NULL, E_CARD_MATCH_NONE, info->closure);
match_search_info_free (info);
return;
}
if (card->nickname)
query_parts[p++] = g_strdup_printf ("(beginswith \"nickname\" \"%s\")", card->nickname);
if (card->name->given && strlen (card->name->given) > 1)
query_parts[p++] = g_strdup_printf ("(contains \"full_name\" \"%s\")", card->name->given);
if (card->name->additional && strlen (card->name->additional) > 1)
query_parts[p++] = g_strdup_printf ("(contains \"full_name\" \"%s\")", card->name->additional);
if (card->name->family && strlen (card->name->family) > 1)
query_parts[p++] = g_strdup_printf ("(contains \"full_name\" \"%s\")", card->name->family);
if (card->email) {
EIterator *iter = e_list_get_iterator (card->email);
while (e_iterator_is_valid (iter) && p < MAX_QUERY_PARTS) {
gchar *addr = g_strdup (e_iterator_get (iter));
if (addr) {
gchar *s = addr;
while (*s) {
if (*s == '@') {
*s = '\0';
break;
}
++s;
}
query_parts[p++] = g_strdup_printf ("(beginswith \"email\" \"%s\")", addr);
g_free (addr);
}
e_iterator_next (iter);
}
}
/* Build up our full query from the parts. */
query_parts[p] = NULL;
qj = g_strjoinv (" ", query_parts);
for(i = 0; query_parts[i] != NULL; i++)
g_free(query_parts[i]);
if (p > 0) {
query = g_strdup_printf ("(or %s)", qj);
g_free (qj);
} else {
query = qj;
}
e_book_simple_query (book, query, simple_query_cb, info);
g_free (query);
}
void
e_card_locate_match (ECard *card, ECardMatchQueryCallback cb, gpointer closure)
{
MatchSearchInfo *info;
g_return_if_fail (card && E_IS_CARD (card));
g_return_if_fail (cb != NULL);
info = g_new (MatchSearchInfo, 1);
info->card = card;
gtk_object_ref (GTK_OBJECT (card));
info->cb = cb;
info->closure = closure;
info->avoid = NULL;
e_book_use_local_address_book (use_common_book_cb, info);
}
/**
* e_card_locate_match_full:
* @book: The book to look in. If this is NULL, use the main local
* addressbook.
* @card: The card to compare to.
* @avoid: A list of cards to not match. These will not show up in the search.
* @cb: The function to call.
* @closure: The closure to add to the call.
*
* Look for the best match and return it using the ECardMatchQueryCallback.
**/
void
e_card_locate_match_full (EBook *book, ECard *card, GList *avoid, ECardMatchQueryCallback cb, gpointer closure)
{
MatchSearchInfo *info;
g_return_if_fail (card && E_IS_CARD (card));
g_return_if_fail (cb != NULL);
info = g_new (MatchSearchInfo, 1);
info->card = card;
gtk_object_ref (GTK_OBJECT (card));
info->cb = cb;
info->closure = closure;
info->avoid = g_list_copy (avoid);
g_list_foreach (info->avoid, (GFunc) gtk_object_ref, NULL);
if (book)
use_common_book_cb (book, info);
else
e_book_use_local_address_book (use_common_book_cb, info);
}