<commit_before><?php

/**
 * Class CRM_BlendleImport_Import_MatchFinder.
 * Find matches for import records.
 * These functions are inspired by CsvImportHelper->findContact.
 *
 * @author <NAME> <<EMAIL>>
 * @package org.decooperatie.blendleimport
 * @license AGPL-3.0
 */
class CRM_BlendleImport_Import_MatchFinder {

  /**
   * @var static Class instance
   */
  protected static $instance;

  /**
   * @return static Class instance
   */
  public static function instance() {
    if (!static::$instance) {
      static::$instance = new static;
    }
    return self::$instance;
  }

  /**
   * Find matches based on a record's byline field, and store results
   * in the import record's 'status' and 'resolution' fields.
   * @param CRM_BlendleImport_BAO_ImportRecord $record
   * @return bool Match found?
   */
  public function match(CRM_BlendleImport_BAO_ImportRecord &$record) {

    // Parse names (cleanup is now also executed while importing CSV, but we still need to parse first/last name)
    $names = self::cleanupName($record->byline, $record->title);

    // Store updated name with button / publication rules applied to show in listing
    if($names === FALSE || empty($names)) {
      $record->byline = '[Onbekend!]';
      $this->storeResult([], $record, 'Name field empty');
      return FALSE;
    } else {
      $record->byline = implode(' ', $names);
    }

    // Clean up voorvoegsels / middle names for matching
    $names['last'] = str_replace(['van ', 'de ', 'der ', 'den ', 'te ', '\'t ', 't ', 'v ', 'en '], '', $names['last']);

    // 1. Try to find contact by full literal first and last name
    $result = $this->findContacts($names['last'], $names['first']);
    if ($result) {
      $this->storeResult($result, $record, 'First and last name');
      return TRUE;
    }

    // 2. Try to find contact(s) by literal last name only
    $result = $this->findContacts($names['last'], NULL);
    if ($result) {
      $this->storeResult($result, $record, 'Last name');
      return TRUE;
    }

    // 3. Try to find contacts by %first word of first name% AND %last word of last name% with wildcard
    if (preg_match('/^([^ ]+)/', $names['first'], $fMatches)) {
      if (preg_match('/([^ ]+)$/', $names['last'], $lMatches)) {
        // error_log("Checking matches for partial first and last name: 1 {$names['first']} 2 {$names['last']} 3 {$lMatches[1]} 4 {$fMatches[1]}.");
        $result = $this->findContacts($lMatches[1], $fMatches[1], TRUE);
        if ($result) {
          $this->storeResult($result, $record, 'Partial first and last name');
          return TRUE;
        }
      }
    }

    // 4. Try to find contact(s) by %last name% only
    $result = $this->findContacts($names['last'], NULL, TRUE);
    if ($result) {
      $this->storeResult($result, $record, 'Last name with wildcard');
      return TRUE;
    }

    // 5. Try to find contact(s) by %last word of last name% only
    if (preg_match('/([^ ]+) (.+)/', $names['last'], $matches)) {
      $result = $this->findContacts($matches[2], NULL, TRUE);
      if ($result) {
        $this->storeResult($result, $record, 'Last word of last name');
        return TRUE;
      }
    }

    // Nothing found
    $this->storeResult([], $record, 'No matches found');
    return FALSE;
  }

  /**
   * Try to find a name in byline and/or title and return an array with [first, last] name.
   * This function is both called above and when creating contacts.
   * @param string|null $names Article Byline
   * @param string|null $title Article Title
   * @return array|bool Array with first/last name, or false if name was empty.
   */
  public static function cleanupName($names, $title = NULL) {

    $names = trim($names);

    // If no byline but [Button] in title...
    if (empty($names) && !empty($title) && preg_match('/^\[Button\]([- ]+)?([^-]+)[- ].*$/', trim($title), $matches)) {
      $names = trim($matches[2]);
    }

    // No name at all?
    if (empty($names)) {
      return FALSE;
    }

    // Remove /Publication part and 'Tekst: ' from names and replace underscores
    if (preg_match('/^([^\/]+)\/(.*)$/', $names, $matches)) {
      $names = $matches[1];
    }
    $names = str_ireplace('Tekst: ', '', $names);
    $names = preg_replace('/_+/', ' ', $names);

    // Check for name format: Last, First - otherwise assume First Last [Last Last]
    if (preg_match('/^([^,]+)\s*,\s*([^,]+)$/', $names, $matches)) {
      $lastName = $matches[1];
      $firstName = trim($matches[2]);
    } else {
      $splitNames = preg_split('/\s+/', $names);
      if (count($splitNames) == 1) {
        $lastName = array_shift($splitNames);
        $firstName = NULL;
      } else {
        $firstName = array_shift($splitNames);
        $lastName = implode(' ', $splitNames);
      }
    }

    // Return found [first, last] names
    return ['first' => $firstName, 'last' => $lastName];
  }

  /**<commit_msg>Add extra first name + last name matching rule<commit_after><?php

/**
 * Class CRM_BlendleImport_Import_MatchFinder.
 * Find matches for import records.
 * These functions are inspired by CsvImportHelper->findContact.
 *
 * @author <NAME> <<EMAIL>>
 * @package org.decooperatie.blendleimport
 * @license AGPL-3.0
 */
class CRM_BlendleImport_Import_MatchFinder {

  /**
   * @var static Class instance
   */
  protected static $instance;

  /**
   * @return static Class instance
   */
  public static function instance() {
    if (!static::$instance) {
      static::$instance = new static;
    }
    return self::$instance;
  }

  /**
   * Find matches based on a record's byline field, and store results
   * in the import record's 'status' and 'resolution' fields.
   * @param CRM_BlendleImport_BAO_ImportRecord $record
   * @return bool Match found?
   */
  public function match(CRM_BlendleImport_BAO_ImportRecord &$record) {

    // Parse names (cleanup is now also executed while importing CSV, but we still need to parse first/last name)
    $names = self::cleanupName($record->byline, $record->title);

    // Store updated name with button / publication rules applied to show in listing
    if($names === FALSE || empty($names)) {
      $record->byline = '[Onbekend!]';
      $this->storeResult([], $record, 'Name field empty');
      return FALSE;
    } else {
      $record->byline = implode(' ', $names);
    }

    // Clean up voorvoegsels / middle names for matching
    $names['last'] = str_replace(['van ', 'de ', 'der ', 'den ', 'te ', '\'t ', 't ', 'v ', 'en '], '', $names['last']);

    // 1. Try to find contact by full literal first and last name
    $result = $this->findContacts($names['last'], $names['first']);
    if ($result) {
      $this->storeResult($result, $record, 'First and last name');
      return TRUE;
    }

    // 2. Try to find contact(s) by full first and last name with wildcard
    $result = $this->findContacts($names['last'], $names['first'], TRUE);
    if ($result) {
      $this->storeResult($result, $record, 'First and last name with wildcard');
      return TRUE;
    }

    // 3. Try to find contact(s) by literal last name only
    $result = $this->findContacts($names['last'], NULL);
    if ($result) {
      $this->storeResult($result, $record, 'Last name');
      return TRUE;
    }

    // 4. Try to find contacts by %first word of first name% AND %last word of last name% with wildcard
    if (preg_match('/^([^ ]+)/', $names['first'], $fMatches)) {
      if (preg_match('/([^ ]+)$/', $names['last'], $lMatches)) {
        // error_log("Checking matches for partial first and last name: 1 {$names['first']} 2 {$names['last']} 3 {$lMatches[1]} 4 {$fMatches[1]}.");
        $result = $this->findContacts($lMatches[1], $fMatches[1], TRUE);
        if ($result) {
          $this->storeResult($result, $record, 'Partial first and last name');
          return TRUE;
        }
      }
    }

    // 5. Try to find contact(s) by %last name% only
    $result = $this->findContacts($names['last'], NULL, TRUE);
    if ($result) {
      $this->storeResult($result, $record, 'Last name with wildcard');
      return TRUE;
    }

    // 6. Try to find contact(s) by %last word of last name% only
    if (preg_match('/([^ ]+) (.+)/', $names['last'], $matches)) {
      $result = $this->findContacts($matches[2], NULL, TRUE);
      if ($result) {
        $this->storeResult($result, $record, 'Last word of last name');
        return TRUE;
      }
    }

    // Nothing found
    $this->storeResult([], $record, 'No matches found');
    return FALSE;
  }

  /**
   * Try to find a name in byline and/or title and return an array with [first, last] name.
   * This function is both called above and when creating contacts.
   * @param string|null $names Article Byline
   * @param string|null $title Article Title
   * @return array|bool Array with first/last name, or false if name was empty.
   */
  public static function cleanupName($names, $title = NULL) {

    $names = trim($names);

    // If no byline but [Button] in title...
    if (empty($names) && !empty($title) && preg_match('/^\[Button\]([- ]+)?([^-]+)[- ].*$/', trim($title), $matches)) {
      $names = trim($matches[2]);
    }

    // No name at all?
    if (empty($names)) {
      return FALSE;
    }

    // Remove /Publication part and 'Tekst: ' from names and replace underscores
    if (preg_match('/^([^\/]+)\/(.*)$/', $names, $matches)) {
      $names = $matches[1];
    }
    $names = str_ireplace('Tekst: ', '', $names);
    $names = preg_replace('/_+/', ' ', $names);

    // Check for name format: Last, First - otherwise assume First Last [Last Last]
    if (preg_match('/^([^,]+)\s*,\s*([^,]+)$/', $names, $matches)) {
      $lastName = trim($matches[1]);
      $firstName = trim($matches[2]);
    } else {
      $splitNames = preg_split('/\s+/', $names);
      if (count($splitNames) == 1) {
        $lastName = array_shift($splitNames);
        $firstName = NULL;
      } else {
        $firstName = array_shift($splitNames);
        $lastName = implode(' ', $splitNames);
      }
    }

    // Return found [first, last] names
    return ['first' => $firstName, 'last' => $lastName];
  }

  /**
{'fraction_non_alphanumeric': 0.17495069033530572, 'fraction_numerical': 0.004240631163708087, 'mean_word_length': 2.8914044512663084, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1997304', 'n_tokens_mistral': 3243, 'n_tokens_neox': 3160, 'n_words': 952}