Fix duplicate company/service contacts: robust phone matching
Firmen-/Service-Eintraege (nur Nummer + Firma, kein Name/E-Mail) wurden bei jedem Sync neu angelegt statt verknuepft, weil das Matching Rufnummern feldgenau und formatabhaengig verglich. Bei unterschiedlich formatierten Nummern (+49 vs 0) und wechselnden Feldern (geschaeftlich/privat) schlug der Abgleich fehl -> Vervielfachung. - NormalizePhone vereinheitlicht jetzt +49/0049 -> 0 (SyncEngine + ContactMerger). - Telefon-Abgleich feldUEBERGREIFEND (jede Nummer gegen jede) via SharedPhone. - "Nummer + Firma"-Treffer nur noch fuer Eintraege OHNE Personennamen, damit Kollegen mit gemeinsamer Zentrale-Nummer nicht verschmolzen werden. - Zusaetzlich: reine Nummern-Eintraege (kein Name/Firma/E-Mail) matchen ueber eine gemeinsame Rufnummer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,16 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
|
|||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
|
- **Dubletten bei Firmen-/Service-Eintraegen (nur Nummer + Firma).** Solche
|
||||||
|
Eintraege wurden bei jedem Sync neu angelegt statt verknuepft, weil der
|
||||||
|
Nummern-Abgleich feldgenau (geschaeftlich↔geschaeftlich) und formatabhaengig
|
||||||
|
war. Jetzt werden Rufnummern feldUEBERGREIFEND und formatunabhaengig
|
||||||
|
verglichen (`+49`/`0049` == `0`), sodass diese Eintraege verknuepft werden.
|
||||||
|
Der "Nummer + Firma"-Abgleich greift bewusst nur bei Eintraegen OHNE
|
||||||
|
Personennamen, damit keine zwei Kollegen mit gleicher Zentrale-Nummer
|
||||||
|
verschmolzen werden. (Bereits entstandene Dubletten muessen einmalig manuell
|
||||||
|
bereinigt werden.)
|
||||||
|
|
||||||
- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende
|
- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende
|
||||||
Ursachen wurden beseitigt:
|
Ursachen wurden beseitigt:
|
||||||
- Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen
|
- Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen
|
||||||
|
|||||||
@@ -140,7 +140,11 @@ namespace StarfaceOutlookSync.Services
|
|||||||
private static string NormalizePhone(string phone)
|
private static string NormalizePhone(string phone)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(phone)) return "";
|
if (string.IsNullOrEmpty(phone)) return "";
|
||||||
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
||||||
|
if (s.StartsWith("+49")) s = "0" + s.Substring(3);
|
||||||
|
else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
|
||||||
|
else if (s.StartsWith("+")) s = s.Substring(1);
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,9 @@ namespace StarfaceOutlookSync.Services
|
|||||||
bool nameMatch = hasName
|
bool nameMatch = hasName
|
||||||
&& (a.FirstName ?? "").Equals(b.FirstName ?? "", StringComparison.OrdinalIgnoreCase)
|
&& (a.FirstName ?? "").Equals(b.FirstName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||||
&& (a.LastName ?? "").Equals(b.LastName ?? "", StringComparison.OrdinalIgnoreCase);
|
&& (a.LastName ?? "").Equals(b.LastName ?? "", StringComparison.OrdinalIgnoreCase);
|
||||||
bool phoneMatch = (!string.IsNullOrEmpty(a.PhoneWork) && !string.IsNullOrEmpty(b.PhoneWork)
|
// Telefon feldUEBERGREIFEND vergleichen (die gleiche Nummer steht mal
|
||||||
&& NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork))
|
// als geschaeftlich, mal als privat) und formatunabhaengig (+49 == 0).
|
||||||
|| (!string.IsNullOrEmpty(a.PhoneMobile) && !string.IsNullOrEmpty(b.PhoneMobile)
|
bool phoneMatch = SharedPhone(a, b);
|
||||||
&& NormalizePhone(a.PhoneMobile) == NormalizePhone(b.PhoneMobile))
|
|
||||||
|| (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax)
|
|
||||||
&& NormalizePhone(a.Fax) == NormalizePhone(b.Fax));
|
|
||||||
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
|
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
|
||||||
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
|
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -86,17 +83,54 @@ namespace StarfaceOutlookSync.Services
|
|||||||
// (Telefon-Umformatierung durch Starface darf einen Namens-Treffer NICHT verhindern.)
|
// (Telefon-Umformatierung durch Starface darf einen Namens-Treffer NICHT verhindern.)
|
||||||
if (nameMatch && !emailContradiction) return true;
|
if (nameMatch && !emailContradiction) return true;
|
||||||
|
|
||||||
// Schwacher Pfad: Telefon/Fax nur zusammen mit gleicher Firma und ohne E-Mail-Widerspruch.
|
// Firma-/Service-Eintraege OHNE Personennamen: gleiche Firma + gemeinsame
|
||||||
if (phoneMatch && companyMatch && !emailContradiction) return true;
|
// Nummer. Bewusst nur ohne Namen, damit nicht zwei Kollegen mit gleicher
|
||||||
|
// Zentrale-Nummer + Firma faelschlich verschmolzen werden.
|
||||||
|
bool aHasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName);
|
||||||
|
bool bHasName = !string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName);
|
||||||
|
if (phoneMatch && companyMatch && !aHasName && !bHasName && !emailContradiction) return true;
|
||||||
|
|
||||||
|
// Reine Nummern-Eintraege (kein Name, keine Firma, keine E-Mail auf beiden
|
||||||
|
// Seiten): eine gemeinsame Rufnummer identifiziert den Eintrag.
|
||||||
|
if (phoneMatch && IsBareNumberEntry(a) && IsBareNumberEntry(b)) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsBareNumberEntry(UnifiedContact c) =>
|
||||||
|
string.IsNullOrEmpty(c.FirstName) && string.IsNullOrEmpty(c.LastName)
|
||||||
|
&& string.IsNullOrEmpty(c.Company) && string.IsNullOrEmpty(c.Email);
|
||||||
|
|
||||||
|
/// <summary>True, wenn a und b mindestens eine (normalisierte) Rufnummer gemeinsam haben.</summary>
|
||||||
|
private static bool SharedPhone(UnifiedContact a, UnifiedContact b)
|
||||||
|
{
|
||||||
|
var pa = PhoneSet(a);
|
||||||
|
if (pa.Count == 0) return false;
|
||||||
|
var pb = PhoneSet(b);
|
||||||
|
return pb.Count > 0 && pa.Overlaps(pb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<string> PhoneSet(UnifiedContact c)
|
||||||
|
{
|
||||||
|
var set = new HashSet<string>();
|
||||||
|
foreach (var p in new[] { c.PhoneWork, c.PhoneMobile, c.PhoneHome, c.Fax })
|
||||||
|
{
|
||||||
|
var n = NormalizePhone(p);
|
||||||
|
if (n.Length >= 5) set.Add(n); // zu kurze (z.B. reine Durchwahlen) ignorieren
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
private static string NormalizePhone(string phone)
|
private static string NormalizePhone(string phone)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(phone)) return "";
|
if (string.IsNullOrEmpty(phone)) return "";
|
||||||
// Nur Ziffern und + behalten
|
// Nur Ziffern und + behalten
|
||||||
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
||||||
|
// Landesvorwahl vereinheitlichen, damit +49/0049 und 0 gleich sind.
|
||||||
|
if (s.StartsWith("+49")) s = "0" + s.Substring(3);
|
||||||
|
else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
|
||||||
|
else if (s.StartsWith("+")) s = s.Substring(1);
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
|
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
|
||||||
|
|||||||
Reference in New Issue
Block a user