868d6abb7 2025 -> 2026
30ec0db73 AddressImportReconciler
cab29b70b WIP : AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
48e37a4c9 AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
0b1ab35a7 AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.
868d6abb7
2025 -> 2026868d6abb75cd191a892911ac8e45058932cf9074
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index 6c334b22b..a4d96916a 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -3,7 +3,7 @@
// Projet ProjectForge Community Edition
// www.projectforge.org
//
-// Copyright (C) 2001-2025 Micromata GmbH, Allemagne (www.micromata.com)
+// Copyright (C) 2001-2026 Micromata GmbH, Allemagne (www.micromata.com)
//
// ProjectForge est sous double licence.
//
30ec0db73
AddressImportReconciler30ec0db73e57c418559d0d754bfceb90c1997db2
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index c2f8e1068..6c334b22b 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -70,6 +70,13 @@ object AddressTextParser {
RegexOption.IGNORE_CASE
)
+ // Regex téléphone nu (numéro de téléphone sans étiquette de préfixe)
+ // Correspond aux numéros de téléphone qui commencent par + ou un indicatif pays et ont au moins 8 chiffres
+ // Cela évite les faux positifs avec les numéros ordinaires
+ private val BARE_PHONE_REGEX = Regex(
+ """^\+?(?:\d+[\s\-./()]*){8,}$"""
+ )
+
// Code postal + Ville (4-5 chiffres + nom de ville, optionnellement avec préfixe "D-" ou "CH-")
// Prend en charge l'allemand (5 chiffres), le suisse (4 chiffres) et d'autres formats
private val ZIP_CITY_REGEX = Regex(
@@ -135,7 +142,7 @@ object AddressTextParser {
}
}
- // Extraire les numéros de téléphone
+ // Extraire les numéros de téléphone avec préfixe (Tel:, Phone:, etc.)
if (line.matches(Regex(""".*(?:Tel\.?:?|Telefon:?|Phone:?|Fon:?|Mobil:?|Mobile:?|Fax:?).*""", RegexOption.IGNORE_CASE))) {
val phoneMatch = PHONE_REGEX.find(line)
if (phoneMatch != null) {
@@ -161,6 +168,25 @@ object AddressTextParser {
}
}
+ // Extraire les numéros de téléphone nus (sans préfixe)
+ if (!processed && BARE_PHONE_REGEX.matches(line)) {
+ val phone = line.trim()
+ phoneNumbers.add(phone)
+
+ // Normaliser le numéro de téléphone
+ val normalizedPhone = org.projectforge.framework.utils.PhoneNumberUtils.normalizePhoneNumber(phone)
+
+ // Sans étiquette, supposer qu'il s'agit d'un téléphone professionnel (sauf si nous en avons déjà un, alors mobile)
+ if (result.businessPhone == null) {
+ result.businessPhone = normalizedPhone
+ } else if (result.mobilePhone == null) {
+ result.mobilePhone = normalizedPhone
+ } else if (result.fax == null) {
+ result.fax = normalizedPhone
+ }
+ processed = true
+ }
+
// Extraire le code postal + Ville (peut être combiné avec la rue dans la même ligne)
ZIP_CITY_REGEX.find(line)?.let {
if (result.zipCode == null) { cab29b70b
WIP : AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.cab29b70bfb32b34408aae8db933f60ec04bc405
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index 9b55d3d6a..c2f8e1068 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -55,9 +55,10 @@ object AddressTextParser {
RegexOption.IGNORE_CASE
)
- // Regex site web
+ // Regex site web avec TLD connus pour éviter les faux positifs comme "Dipl.Phys"
+ // Note : Les TLD plus longs sont listés avant les plus courts pour garantir une correspondance correcte (ex. "group" avant "gr")
private val WEBSITE_REGEX = Regex(
- """(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s]*)?""",
+ """(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.(?:solutions|company|academy|digital|center|online|store|group|cloud|gmbh|tech|info|shop|app|dev|pro|com|org|net|edu|gov|biz|aero|asia|coop|jobs|mobi|museum|name|post|tel|travel|xxx|de|uk|us|io|co|eu|ch|at|fr|it|es|nl|be|pl|ru|jp|cn|au|ca|nz|se|no|dk|fi|in|br|mx|za|kr|tw|hk|sg|my|th|vn|ph|id|ae|sa|il|tr|gr|cz|sk|hu|ro|bg|hr|si|lt|lv|ee|is|ie|pt|lu|mt|cy|ai)(?:/[^\s]*)?""",
RegexOption.IGNORE_CASE
)
@@ -118,10 +119,18 @@ object AddressTextParser {
// Extraire le site web (mais pas l'email)
if (!line.contains("@")) {
- WEBSITE_REGEX.find(line)?.let {
- if (result.website == null && !it.value.contains("@")) {
- result.website = it.value
- processed = true
+ WEBSITE_REGEX.find(line)?.let { match ->
+ if (result.website == null && !match.value.contains("@")) {
+ // Vérification supplémentaire : Si 2+ mots suivent la correspondance, il s'agit probablement d'un nom avec titre (ex. "Dipl.Phys Max Mustermann")
+ val remainingText = line.substring(match.range.last + 1).trim()
+ val wordsAfter = remainingText.split(Regex("""\s+""")).filter { it.isNotBlank() }
+
+ if (wordsAfter.size < 2) {
+ // Probablement un vrai site web
+ result.website = match.value
+ processed = true
+ }
+ // Si 2+ mots suivent, ignorer cette correspondance (probablement un titre + nom)
}
}
}
@@ -285,6 +294,11 @@ object AddressTextParser {
result.title = parsedName.titles.joinToString(" ")
}
+ // Définir la formule de politesse (correspond à AddressDO.form)
+ if (parsedName.formOfAddress != null) {
+ result.form = parsedName.formOfAddress
+ }
+
// Définir le prénom et le nom de famille
if (parsedName.firstName.isNotEmpty()) {
result.firstName = parsedName.firstName 48e37a4c9
AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.48e37a4c92a17e99e7d8d7440d4d35a1fcabf3ce
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index 307a08040..9b55d3d6a 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -277,8 +277,8 @@ object AddressTextParser {
remainingLine = remainingLine.substring(iAMatch.value.length).trim()
}
- // Utiliser NameParser pour extraire les titres, formules de politesse, prénom et nom
- val parsedName = NameParser.parse(remainingLine)
+ // Utiliser PersonNameParser pour extraire les titres, formules de politesse, prénom et nom
+ val parsedName = PersonNameParser.parse(remainingLine)
// Définir le titre (joindre tous les titres avec un espace)
if (parsedName.titles.isNotEmpty()) { 0b1ab35a7
AddressTextParser : PersonNameParser introduit pour une meilleure analyse des titres, formules de politesse, etc.0b1ab35a73e093d9af27668996430ed46819a4ba
diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
index fa1e7e4ab..307a08040 100644
--- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
+++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/AddressTextParser.kt
@@ -32,21 +32,6 @@ private val log = KotlinLogging.logger {}
*/
object AddressTextParser {
- // Titres académiques et professionnels courants en allemand et anglais
- private val TITLE_PATTERNS = listOf(
- "Dr\\.",
- "Prof\\.",
- "Dipl\\.-Kfm\\.",
- "Dipl\\.-Ing\\.",
- "Dipl\\.-Inf\\.",
- "Dipl\\.",
- "B\\.Sc\\.",
- "M\\.Sc\\.",
- "B\\.A\\.",
- "M\\.A\\.",
- "Ph\\.D\\.",
- "MBA"
- )
// Suffixes d'entreprise
private val COMPANY_SUFFIXES = listOf(
@@ -292,27 +277,20 @@ object AddressTextParser {
remainingLine = remainingLine.substring(iAMatch.value.length).trim()
}
- // Extraire le titre si présent
- for (titlePattern in TITLE_PATTERNS) {
- val titleRegex = Regex("""^($titlePattern)\s*""")
- val match = titleRegex.find(remainingLine)
- if (match != null) {
- result.title = match.groupValues[1]
- remainingLine = remainingLine.substring(match.value.length).trim()
- break
- }
+ // Utiliser NameParser pour extraire les titres, formules de politesse, prénom et nom
+ val parsedName = NameParser.parse(remainingLine)
+
+ // Définir le titre (joindre tous les titres avec un espace)
+ if (parsedName.titles.isNotEmpty()) {
+ result.title = parsedName.titles.joinToString(" ")
}
- // Diviser le reste en prénom et nom de famille
- val nameParts = remainingLine.split(Regex("""\s+"""))
- when {
- nameParts.size >= 2 -> {
- result.firstName = nameParts[0]
- result.name = nameParts.drop(1).joinToString(" ")
- }
- nameParts.size == 1 -> {
- result.name = nameParts[0]
- }
+ // Définir le prénom et le nom de famille
+ if (parsedName.firstName.isNotEmpty()) {