Skip to content

Commit

Permalink
Add Finder::find...ByName() (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlocati authored Jul 1, 2024
1 parent 1715279 commit 25ac5bd
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 50 deletions.
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ You can have a list of all the Geographical Subdivisions, Regions, Provinces/UTS
For example:

```php

use MLocati\ComuniItaliani\Factory;

$factory = new Factory();
Expand All @@ -101,7 +100,6 @@ $allMunicipalities = $factory->getMunicipalities();
If you want to retrieve a territory given its ID, you can use the `Finder` class:

```php

use MLocati\ComuniItaliani\Finder;

$finder = new Finder();
Expand All @@ -111,3 +109,40 @@ $region = $finder->getRegionByID('01');
$province = $finder->getProvinceByID('201');
$municipality = $finder->getMunicipalityByID('001272');
```

### Finding territories by name

You can use the `Finder` class to find Geographical Subdivisions, Regions, Provinces/UTS, and Municipalities by name.

The text to be searched will be split into words, and you'll get the territories whose names contain all the words.

For example, searching for `roma lombard` will return the `Romano di Lombardia (BG)` municipality.

Examples:

```php
use MLocati\ComuniItaliani\Finder;

$finder = new Finder();

$geographicalSubdivisions = $finder->findGeographicalSubdivisionsByName('Nord');
$regions = $finder->findRegionsByName('Campa');
$provinces = $finder->findProvincesByName('Bozen');
$municipalities = $finder->findMunicipalitiesByName('Roma lombard');
```

By default, Finder will look for the beginning of words.
So, `ampania` won't match `Campania`.

If you want to allow searching in the middle of the words, specify `true` as the second parameter:

```php
use MLocati\ComuniItaliani\Finder;

$finder = new Finder();

$geographicalSubdivisions = $finder->findGeographicalSubdivisionsByName('ord', true);
$regions = $finder->findRegionsByName('ampania', true);
$provinces = $finder->findProvincesByName('ozen', true);
$municipalities = $finder->findMunicipalitiesByName('oma ombard', true);
```
185 changes: 160 additions & 25 deletions src/Finder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

namespace MLocati\ComuniItaliani;

use Generator;

class Finder
{
use Service\SorterTrait;

protected Factory $factory;

public function __construct(?Factory $factory = null)
Expand Down Expand Up @@ -43,7 +47,7 @@ public function getTerritoryByID($id): ?Territory
*/
public function getGeographicalSubdivisionByID(int $id): ?GeographicalSubdivision
{
foreach ($this->factory->getGeographicalSubdivisions() as $geographicalSubdivision) {
foreach ($this->listGeographicalSubdivisions() as $geographicalSubdivision) {
if ($geographicalSubdivision->getID() === $id) {
return $geographicalSubdivision;
}
Expand All @@ -60,11 +64,9 @@ public function getRegionByID(string $id): ?Region
if (!preg_match('/^[0-9]{2}$/', $id)) {
return null;
}
foreach ($this->factory->getGeographicalSubdivisions() as $geographicalSubdivisions) {
foreach ($geographicalSubdivisions->getRegions() as $region) {
if ($region->getID() === $id) {
return $region;
}
foreach ($this->listRegions() as $region) {
if ($region->getID() === $id) {
return $region;
}
}

Expand All @@ -81,18 +83,12 @@ public function getProvinceByID(string $id, bool $oldIDToo = false): ?Province
if (!preg_match('/^[0-9]{3}$/', $id)) {
return null;
}
foreach ($this->factory->getGeographicalSubdivisions() as $geographicalSubdivisions) {
foreach ($geographicalSubdivisions->getRegions() as $region) {
foreach ($region->getProvinces() as $province) {
foreach ($province->getMunicipalities() as $municipality) {
if ($province->getID() === $id) {
return $province;
}
if ($oldIDToo && $province->getOldID() === $id) {
return $province;
}
}
}
foreach ($this->listProvinces() as $province) {
if ($province->getID() === $id) {
return $province;
}
if ($oldIDToo && $province->getOldID() === $id) {
return $province;
}
}

Expand All @@ -110,14 +106,153 @@ public function getMunicipalityByID(string $id): ?Municipality
if (!preg_match('/^[0-9]{6}$/', $id)) {
return null;
}
foreach ($this->listMunicipalities() as $municipality) {
if ($municipality->getID() === $id) {
return $municipality;
}
}

return null;
}

/**
* @return \MLocati\ComuniItaliani\GeographicalSubdivision[]
*/
public function findGeographicalSubdivisionsByName(string $name, bool $allowMiddle = false): array
{
return $this->findByName($name, $this->listGeographicalSubdivisions(), $allowMiddle, false);
}

/**
* @return \MLocati\ComuniItaliani\Region[]
*/
public function findRegionsByName(string $name, bool $allowMiddle = false): array
{
return $this->findByName($name, $this->listRegions(), $allowMiddle, true);
}

/**
* @return \MLocati\ComuniItaliani\Province[]
*/
public function findProvincesByName(string $name, bool $allowMiddle = false): array
{
return $this->findByName($name, $this->listProvinces(), $allowMiddle, true);
}

/**
* @return \MLocati\ComuniItaliani\Municipality[]
*/
public function findMunicipalitiesByName(string $name, bool $allowMiddle = false): array
{
return $this->findByName($name, $this->listMunicipalities(), $allowMiddle, true);
}

/**
* @param \Generator<\MLocati\ComuniItaliani\Territory> $lister
*
* @return \MLocati\ComuniItaliani\Territory[]
*/
protected function findByName(string $name, Generator $lister, bool $allowMiddle, bool $sort): array
{
if (($wantedWords = $this->extractWords($name)) === []) {
return [];
}
$result = [];
foreach ($lister as $territory) {
$territoryWords = $this->extractWords((string) $territory);
if ($this->matchesWords($wantedWords, $territoryWords, $allowMiddle)) {
$result[] = $territory;
}
}

return $sort ? $this->sortTerritoriesByName($result) : $result;
}

/**
* @return \Generator<\MLocati\ComuniItaliani\GeographicalSubdivision>
*/
protected function listGeographicalSubdivisions(): Generator
{
foreach ($this->factory->getGeographicalSubdivisions() as $geographicalSubdivisions) {
foreach ($geographicalSubdivisions->getRegions() as $region) {
foreach ($region->getProvinces() as $province) {
foreach ($province->getMunicipalities() as $municipality) {
if ($municipality->getID() === $id) {
return $municipality;
}
}
yield $geographicalSubdivisions;
}
}

/**
* @return \Generator<\MLocati\ComuniItaliani\Region>
*/
protected function listRegions(): Generator
{
foreach ($this->listGeographicalSubdivisions() as $geographicalSubdivision) {
foreach ($geographicalSubdivision->getRegions() as $region) {
yield $region;
}
}
}

/**
* @return \Generator<\MLocati\ComuniItaliani\Province>
*/
protected function listProvinces(): Generator
{
foreach ($this->listRegions() as $region) {
foreach ($region->getProvinces() as $province) {
yield $province;
}
}
}

/**
* @return \Generator<\MLocati\ComuniItaliani\Municipality>
*/
protected function listMunicipalities(): Generator
{
foreach ($this->listProvinces() as $province) {
foreach ($province->getMunicipalities() as $municipality) {
yield $municipality;
}
}
}

/**
* @return string[]
*/
protected function extractWords(string $name): array
{
$name = strtr($name, self::getMultibyteToAsciiMap());
$name = strtolower($name);
$words = preg_split('/[^a-z]+/', $name, -1, PREG_SPLIT_NO_EMPTY);

return $words;
}

/**
* @param string[] $wantedWords
* @param string[] $territoryWords
*/
protected function matchesWords(array $wantedWords, array $territoryWords, bool $allowMiddle): bool
{
$index = -1;
foreach ($wantedWords as $wantedWord) {
$index = $this->matchesWord($wantedWord, $territoryWords, $allowMiddle, $index);
if ($index === null) {
return false;
}
}

return true;
}

/**
* @param string[] $territoryWords
*/
protected function matchesWord(string $wantedWord, array $territoryWords, bool $allowMiddle, int $afterIndex): ?int
{
foreach ($territoryWords as $index => $territoryWord) {
if ($index > $afterIndex) {
$found = strpos($territoryWord, $wantedWord);
if ($found === 0 || ($allowMiddle && $found !== false)) {
return $index;
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions src/Service/MultibyteTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace MLocati\ComuniItaliani\Service;

trait MultibyteTrait
{
protected static function getMultibyteToAsciiMap(): array
{
return [
"\u{e0}" => 'a', // à
"\u{c0}" => 'A', // À
"\u{e2}" => 'a', // â
"\u{c2}" => 'A', // Â
"\u{e7}" => 'c', // ç
"\u{c7}" => 'C', // Ç
"\u{10d}" => 'c', // č
"\u{10c}" => 'C', // Č
"\u{e8}" => 'e', // è
"\u{c8}" => 'E', // È
"\u{e9}" => 'e', // é
"\u{c9}" => 'E', // É
"\u{ea}" => 'e', // ê
"\u{ca}" => 'E', // Ê
"\u{ec}" => 'i', // ì
"\u{cc}" => 'I', // Ì
"\u{f2}" => 'o', // ò
"\u{d2}" => 'O', // Ò
"\u{f6}" => 'o', // ö
"\u{d6}" => 'O', // Ö
"\u{f4}" => 'o', // ô
"\u{d4}" => 'O', // Ô
"\u{f9}" => 'u', // ù
"\u{d9}" => 'U', // Ù
"\u{fc}" => 'u', // ü
"\u{dc}" => 'U', // Ü
"\u{df}" => 'ss', // ß
"\u{17e}" => 'z', // ž
"\u{17d}" => 'Z', // Ž
];
}
}
23 changes: 2 additions & 21 deletions src/Service/SorterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

trait SorterTrait
{
use MultibyteTrait;

/**
* @param \MLocati\ComuniItaliani\Territory[] $territories
*
Expand Down Expand Up @@ -53,25 +55,4 @@ protected function sortTerritoriesByNameWithoutCollator(array $territories): arr

return $territories;
}

protected static function getMultibyteToAsciiMap(): array
{
return [
"\u{e0}" => 'a',
"\u{e2}" => 'a',
"\u{e7}" => 'c',
"\u{10d}" => 'c',
"\u{e8}" => 'e',
"\u{e9}" => 'e',
"\u{ea}" => 'e',
"\u{ec}" => 'i',
"\u{f2}" => 'o',
"\u{f6}" => 'o',
"\u{f4}" => 'o',
"\u{f9}" => 'u',
"\u{fc}" => 'u',
"\u{df}" => 'ss',
"\u{17e}" => 'z',
];
}
}
Loading

0 comments on commit 25ac5bd

Please sign in to comment.