Compare commits

...

130 Commits

Author SHA1 Message Date
119f216907 Merge branch 'master' of gitea:museum-digital/MDNodaHelpers 2025-06-08 17:20:24 +02:00
25668b7b16 Ping and reconnect DB in fulltext sync for actors fulltext index 2025-06-08 17:19:47 +02:00
8a31cf216e Add shortened 100x A to list of blacklisted tags 2025-05-22 16:25:27 +02:00
ff474341ed Add iconclass terms BB, CC, DD, to blacklist 2025-05-08 16:18:05 +02:00
1051e10732 Prevent ambigious splitting of [0-9]{4}-[0-9]{2} 2025-05-06 22:32:00 +02:00
057cac0f1b Ensure 1903/1904 cannot be split 2025-05-05 17:05:47 +02:00
0053fbe030 Support splitting times like "1. Hälfte des 19. Jahrhunderts" 2025-04-28 17:00:32 +02:00
7a2856ffad Split times in more cases (300-20 BC, 300-4000 CE) 2025-04-08 15:18:32 +02:00
00638152cf Prevent splitting of non-existing exact dates (e.g. 31.04.XXXX)
Close #35
2025-04-08 03:48:04 +02:00
dba60dbce6 Fix order of split days and months within a single year BCE
Close #32
2025-04-07 18:32:14 +02:00
f84fe1bca5 Fix type error / reference to values now not consistently existing
anymore
2025-04-06 22:56:36 +02:00
423959ac94 Stop early if autotranslation cannot proceed after validation 2025-04-05 00:11:03 +02:00
e8edb4a459 Time splitter: Handle first/second half
Close #31
2025-04-05 00:09:39 +02:00
8491b62a83 Validate against time errors in autogenerating translations for times
Close #30
2025-04-04 20:03:59 +02:00
bb2b1c2c32 Update NodaGroup 2025-03-13 00:30:33 +01:00
5054d3c62f Use more rigurous trimming in NodaConsolidatedNamesForPersinst 2025-03-10 04:18:00 +01:00
beba838c0d Correctly handle multibype hyphens in XXXX-XXXX 2025-03-10 04:13:59 +01:00
54dd958073 See before 2025-03-10 04:05:00 +01:00
5b99304b5c Accept an additional type of hyphen / dash in time splitting 2025-03-10 03:59:44 +01:00
5cce98f15b Extend tests 2025-03-10 03:20:46 +01:00
5036c77f32 Extend test for getting actor ID by life dates + name 2025-03-10 02:18:28 +01:00
e95415be8f Add test for getting actor ID by name with life dates 2025-03-10 01:48:09 +01:00
5192781494 Use Wikipedia API for getting descriptions from Wikipedia rather than
parsing HTML in Wikidata fetcher

Thanks @awinkler
2025-03-09 02:08:26 +01:00
d9d9f7fcdc Continue refactoring tests for time splitter to run provider-based 2025-02-24 14:02:42 +01:00
dbfa0df17f Begin restructuring NodaTimeSplitterTest to use data providers 2025-02-21 10:32:07 +01:00
3409ec7afe Begin adding autotranslation language CRH / Crimean Tatar
Some formatting is still unclear. See https://forum.museum-digital.info/d/52-additional-languages-for-translations-crimean-tatar/9
2025-02-18 17:51:36 +01:00
27ac3f255a Minor typing improvements 2025-02-15 13:36:50 +01:00
9d7d53a858 Disallow fetching from Wikidata disambiguation pages
Close #23
2025-02-13 22:37:17 +01:00
28f6db67ff Disable XML error warnings when parsing unclean inputs from Wikidata 2025-02-13 21:48:07 +01:00
2f3bc5f2fa Prefer wikipedia page titles over wikidata labels
Close #28
2025-02-13 21:38:13 +01:00
39362f537a Merge branch 'master' of gitea:museum-digital/MDNodaHelpers 2025-02-13 17:19:43 +01:00
de0357473a Make constant for test language in NodaWikidataFetcherTest public, allowing reuse 2025-02-13 17:19:06 +01:00
ef43270fb2 Map suffixes material and technique to their respective tag relation
types
2025-02-13 14:04:38 +01:00
338e09f001 Add kannada to list of languages fetched from wikidata 2025-02-13 13:10:45 +01:00
4cf9eaf4fa Remove superfluous params passed to function 2025-02-13 13:10:30 +01:00
18438251a7 Add functions for getting IDs by any translated entry irrespective of
the language
2025-02-12 17:15:19 +01:00
1cf0f9858a Add tests for loading translations in NodaWikidataFetcher 2025-02-12 16:02:04 +01:00
1d50027809 Make function getWikidataEntity public 2025-02-12 15:48:52 +01:00
d1cee17ef5 Add Telugu to list of languages to fetch in Wikidata fetcher
Close #24
2025-02-12 12:47:02 +01:00
baf7905e0b Map gender Q207959
Q207959 is androgyny, mapping is a preliminary solution
2025-02-03 09:41:16 +01:00
9bf14d7d91 Add search function for getting entries in NodaIDGetter across vocabs 2025-01-31 23:25:40 +01:00
a621534136 Update NodaBlacklistedTerms 2025-01-24 13:45:28 +01:00
51fe9a5e45 Cover more edge cases for splitting time names 2025-01-15 11:49:20 +01:00
9c2eaa2929 Allow splitting 1945-48 2025-01-15 10:35:35 +01:00
546c17031a Make NodaImportLogger more resilient, prevent error in case of duplicate import names 2024-12-12 12:43:11 +01:00
bf22f5541d Retrieve "displayed subject" relationship from suffix "<Motiv>", "[Motiv]" 2024-12-03 16:07:41 +01:00
e036d7881a Add missing strict typing in function params 2024-12-01 22:11:17 +01:00
d8db941485 Disallow tags of name "Nichtmünzliches" (de) 2024-11-24 16:08:14 +01:00
b7bb7364d4 Ensure duplicate time names can be parsed in NodaTimeSplitter (e.g.
1.1.2024-1.1.2024)
2024-11-20 10:02:10 +01:00
4dcd93b947 Better validate input JSON fetched from Wikipedia 2024-11-12 15:36:32 +01:00
c72ad51dda Merge branch 'master' of gitea:museum-digital/MDNodaHelpers 2024-11-11 09:11:35 +01:00
d6dea3e280 Remove use of SESSION in NodaWikidataFetcher 2024-11-11 09:11:15 +01:00
6f7ad13c4e Add class NodaTagRelationIdentifier for parsing tag relation types from
input tag names
2024-11-09 19:44:09 +01:00
48355a6a36 Identify uncertainty before brackets ("Berlin ? (Germany)" > "Berlin
(Germany)" + Uncertain)
2024-11-09 18:42:18 +01:00
7cfe752c94 Handle commas when guessing time certainty 2024-11-09 15:40:27 +01:00
29ca05f552 Properly handle commas at the end of names when guessing certainty 2024-11-09 15:33:49 +01:00
eb371d4270 Ensure times can be split despite spaces at random points in given name 2024-10-23 18:02:23 +02:00
16f36c0852 Improve test coverage 2024-10-10 14:32:55 +02:00
669a8a1459 Add tests for lookup functions by vocabulary references 2024-10-10 14:16:52 +02:00
a9c506497c Respect diacritics when looking up tag, actor, .. IDs 2024-10-10 09:51:28 +02:00
06f13c1a71 Add functions for loading only norm data links from Wikidata for places
+ actors
2024-10-03 16:36:30 +02:00
cd49f194f2 Refactor wikidata fetcher 2024-10-03 15:56:31 +02:00
9b63a4d95d Refactor parsing of norm data links from Wikidata into a dedicated
function
2024-10-03 15:03:38 +02:00
96ba020514 Add function for getting actors' names including life dates in batch 2024-09-28 22:45:12 +02:00
c650e57eda Remove references to zoom factor for places in Wikidata fetcher 2024-09-25 15:42:59 +02:00
dea09b17cd Add safeguard against question marks entering NodaSplitTime 2024-08-02 03:41:51 +02:00
cc0997f412 Add direct validation function for noda mail checker 2024-07-28 03:47:35 +02:00
f18e4c3edc Make class constant public 2024-07-19 00:59:21 +02:00
f220a77ad7 Remove linking to wikipedia as a noda repository in wikidata fetcher
Close #21
2024-07-19 00:49:04 +02:00
58d3569718 Fix edge case 200 b.c. in NodaSplitTime 2024-07-08 01:27:03 +02:00
27528c9cf7 Ran phpcbf over code 2024-07-08 00:48:50 +02:00
205e77da0e Remove group members before deleting group 2024-05-27 03:13:03 +02:00
f36938b8dd Add functions for updating / deleting groups 2024-05-27 02:44:28 +02:00
cfa9cee60d Add additional option for logging in nodac 2024-05-23 22:58:42 +02:00
83a557b989 Add additional assertion to NodaTimeSplitterTest for more thorough type safety 2024-05-05 22:54:33 +02:00
7f342ed3c4 Merge branch 'master' of gitea:museum-digital/MDNodaHelpers 2024-05-04 01:19:23 +02:00
7d303e219f Fix broken time splitting for year ranges BC 2024-05-04 01:17:33 +02:00
ce480f8b9f Add validator for tag descriptions 2024-05-01 17:45:07 +02:00
eb14615917 Return start and end date when attempting to split time spans 2024-04-17 00:02:31 +02:00
bd775bec45 Refactor time splitter, support computing of dates for time entries 2024-04-16 23:21:34 +02:00
2cdfa2e948 Merge branch 'master' of gitea:museum-digital/MDNodaHelpers 2024-03-24 02:17:22 +01:00
81a7d64e27 Handle Ukrainian year names (2022 p > 2022) 2024-03-24 00:59:30 +01:00
6af51323e7 Categorize gender Q2449503 2024-03-09 22:49:38 +01:00
3c43a3f2d3 Return an integer when checking time ID by stored rewrite 2024-01-30 00:25:50 +01:00
09518a0a6e Add function for getting time ID by stored rewrite 2024-01-30 00:07:31 +01:00
93f8f13e62 Blacklist tag aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2024-01-15 01:46:21 +01:00
8a8f55b38c Extend NodaGroup to allow adding new ones and linking vocabulary entries
to one
2023-12-12 04:29:23 +01:00
f3831965a3 Extend NodaTimeSplitter 2023-12-09 23:54:45 +01:00
4a49c7a4e7 Fix erroneous splitting of Hungarian time names with additions 2023-12-09 11:56:15 +01:00
3e9f675fdc Add Slovak to list of languages to load 2023-12-08 16:42:24 +01:00
2ab0e75111 Fix erroneous splitting of "YYYY MMD" in Hungarian 2023-12-08 12:26:59 +01:00
27e259072c Add new class NodaGroup 2023-12-07 23:30:24 +01:00
50cb33720b Allow updating and getting names of vocabulary groups 2023-12-07 19:07:11 +01:00
a677c605c5 Use MDMysqli's default blend modes for manticore index setup 2023-12-05 22:10:18 +01:00
681002d844 Extend lists for Ukrainian place hierarchy indicators 2023-12-04 19:22:50 +01:00
40cf5a5112 Set expansion limit, blend_mode in manticore tables 2023-12-04 18:45:24 +01:00
b14f2e14eb Add further indicator for village-level places in Ukrainian 2023-12-03 22:07:32 +01:00
55931ba3ef Cover more levels of Ukrainian (current and historical) place
hierarchies
2023-12-02 16:32:46 +01:00
2badc67405 Add rewriting for Ukrainian place names based on specified hierarchies 2023-12-02 15:21:02 +01:00
b4c941f441 Use MD_STD::json_encode over general \json_encode() 2023-11-27 01:33:53 +01:00
d9a0985feb Extend list of uncertainty prefixes for places with uppercase variants 2023-11-27 01:15:35 +01:00
b36a504277 Add blacklist for unwanted rewrites in consolidating place names 2023-11-26 23:55:43 +01:00
e610723107 Add functions for automatic rewriting of country names to brackets at
the end of place names based on lists
2023-11-26 00:54:14 +01:00
f6409322e5 Add classes for writing consolidating spellings of actor and place names 2023-11-25 22:42:07 +01:00
61e83022ae Extend uncertainty indicator lists with Ukrainian terms
(thanks Ekaterina)
2023-11-25 13:33:50 +01:00
3d58ce3edf Add Ukrainian terms 2023-11-24 12:52:20 +01:00
a33c354ad6 Conform to stricter typing rules 2023-11-21 22:01:28 +01:00
d6c514c208 Add functions to check for actor IDs by name while including their life dates 2023-11-20 13:43:05 +01:00
4496a35f5c Rewrite incomplete time span spellings to extend parsable and splittable time names 2023-11-20 03:18:02 +01:00
78d5137b96 Add Ukrainian uncertainty indicators 2023-11-19 03:53:05 +01:00
a102758606 Extend tests to ensure "vermutl." is included in place uncertainty
indicators
2023-11-18 01:36:01 +01:00
700fefd28c Add "wahrscheinlich" to list of uncertainty indicators for places 2023-11-17 18:32:41 +01:00
4582f6a697 Fix another edge case in time splitter 2023-11-14 03:32:17 +01:00
7ef05a55c5 Migrate PHPunit config to PHPUnit 10's requirements 2023-11-14 03:28:24 +01:00
54a30e683e Add class for loading info from distinctly_typed_strings table 2023-11-13 00:11:56 +01:00
c9b0e7085f Add coverage information to tests, fix coverage of NodaValidationHelper 2023-11-07 23:31:42 +01:00
1a7dbcd6f6 Fix edge cases in time splitter where inputs start with many digits but
are not dates
2023-11-07 00:27:20 +01:00
93c0ff3fa0 Set beStrictAboutOutputDuringTests=true in phpunit.xml 2023-11-06 23:50:12 +01:00
631debcfd8 Add autoloader for tests, phpunit config 2023-11-06 23:46:30 +01:00
53c645b132 Add "vermutl." to list of uncertainty indicators 2023-10-28 21:17:54 +02:00
95de1615ef Identify, parse and remove some more uncertainty indicators 2023-10-27 19:06:08 +02:00
bbbc84015b Fix handling of misassigned lcsh / loc links in NodaWikidataFetcher 2023-10-18 02:46:11 +02:00
d55361e29b Add function to check if a time name is blacklisted 2023-10-18 01:54:40 +02:00
37715bc3e8 Support BCE / CE times 2023-10-15 19:20:16 +02:00
9942c58b12 Improve parsing of LOC / LCSH from Wikidata 2023-09-29 16:20:53 +02:00
0a18449e06 Re-enable infix length in search indexes 2023-09-17 10:59:01 +02:00
efc67b57d3 Remove infix length, increase memory consumed by search indexes 2023-09-17 00:06:42 +02:00
835da05c38 Use wikidata description as fallback if wikipedia description is not
parsable in Wikidata fetcher

Close #16
2023-09-01 12:43:24 +02:00
12a7937218 Comment out debugging lines in NodaWikidataFetcher 2023-08-31 16:11:37 +02:00
a68a03e628 Improve wikidata fetcher 2023-08-31 16:09:21 +02:00
149 changed files with 6256 additions and 1548 deletions

36
phpstan-baseline.neon Normal file
View File

@ -0,0 +1,36 @@
parameters:
ignoreErrors:
-
message: "#^Constant DATABASENAME_NODA not found\\.$#"
count: 3
path: src/NodaBlacklistedTerms.php
-
message: "#^Constant DATABASENAME_NODA not found\\.$#"
count: 2
path: src/NodaMailChecker.php
-
message: "#^Variable \\$timeInfoToCopy in empty\\(\\) always exists and is not falsy\\.$#"
count: 1
path: src/NodaTimeAutotranslater.php
-
message: "#^Call to an undefined method DOMNode\\:\\:getAttribute\\(\\)\\.$#"
count: 1
path: src/NodaWikidataFetcher.php
-
message: "#^Function printHTMLEnd not found\\.$#"
count: 1
path: src/NodaWikidataFetcher.php
-
message: "#^Function write_get_vars not found\\.$#"
count: 9
path: src/NodaWikidataFetcher.php
-
message: "#^Match expression does not handle remaining value\\: string$#"
count: 1
path: src/enums/NodaTimeAutotranslaterLocales.php

View File

@ -8,3 +8,5 @@ parameters:
- ../
ignoreErrors:
excludePaths:
includes:
- phpstan-baseline.neon

14
phpunit.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" backupGlobals="false" beStrictAboutChangesToGlobalState="true" beStrictAboutOutputDuringTests="true" bootstrap="tests/bootstrap.php" cacheResult="false" colors="true" enforceTimeLimit="true" failOnWarning="true" processIsolation="true" stopOnError="true" stopOnFailure="true" stopOnIncomplete="true" stopOnSkipped="true" stopOnRisky="true" testdox="false" timeoutForSmallTests="1" timeoutForMediumTests="10" timeoutForLargeTests="60" cacheDirectory=".phpunit.cache" backupStaticProperties="false" requireCoverageMetadata="false" beStrictAboutCoverageMetadata="false">
<testsuites>
<testsuite name="tests">
<directory>tests/</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View File

@ -0,0 +1,99 @@
<?PHP
/**
* This file contains tools for fetching data from Wikidata.
*
* @file
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
require_once __DIR__ . '/../src/NodaWikidataFetcher.php';
require_once __DIR__ . '/../../MD_STD/src/MD_STD.php';
/**
* Queries wikidata for instances of a Q-ID.
*
* @param string $lang Query language.
* @param string $instanceOf Q-ID.
*
* @return array<mixed>
*/
function query(string $lang, string $instanceOf):array {
$sparqlQueryString = 'SELECT ?item ?itemLabel
WHERE
{
?item wdt:P31/wdt:P279* wd:' . $instanceOf . '.
SERVICE wikibase:label { bd:serviceParam wikibase:language "' . $lang . ',[AUTO_LANGUAGE],en". } # Helps get the label in your language, if not, then en language
}';
return NodaWikidataFetcher::sparqlQuery($sparqlQueryString);
}
/**
* Returns names from a query.
*
* @param array<mixed> $data Wikidata output values.
*
* @return array<string>
*/
function getNames(array $data):array {
$output = [];
foreach ($data['results']['bindings'] as $entry) {
$output[] = $entry['itemLabel']['value'];
}
return $output;
}
// Q6256 => country
$targets = [
'Q6256' => 'countries',
'Q3024240' => 'historical_countries',
'Q10864048' => 'first_lvl_administrative_units',
];
$langs = ['ar', 'bg', 'bn', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fi', 'fr', 'ha', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'ka', 'ko', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'sw', 'ta', 'th', 'tl', 'tr', 'uk', 'ur', 'vi', 'zh'];
foreach ($langs as $lang) {
foreach ($targets as $qid => $filename) {
$regionNames = getNames(query($lang, $qid));
file_put_contents(__DIR__ . '/../static/' . $filename . '.' . $lang . '.json', MD_STD::json_encode($regionNames));
echo "Fetched $lang : $filename ($qid)" . PHP_EOL;
}
}
// The following should be lists of terms that are independent of language
$targetsForMerge = [
'Q23718' => 'cardinal_directions',
];
$mergedValues = [];
foreach ($langs as $lang) {
foreach ($targetsForMerge as $qid => $filename) {
if (!isset($mergedValues[$filename])) {
$mergedValues[$filename] = [];
}
$mergedValues[$filename] = array_merge($mergedValues[$filename], getNames(query($lang, $qid)));
echo "Fetched $lang : $filename ($qid)" . PHP_EOL;
}
}
$mergedValues['cardinal_directions'][] = 'Nord';
$mergedValues['cardinal_directions'][] = 'Ost';
$mergedValues['cardinal_directions'][] = 'West';
$mergedValues['cardinal_directions'][] = 'Süd';
foreach ($mergedValues as $filename => $values) {
file_put_contents(__DIR__ . '/../static/' . $filename . '.json', MD_STD::json_encode(array_values(array_unique($values))));
}

View File

@ -13,7 +13,7 @@ final class NodaBlacklistedTerms {
/**
* A blacklist of disallowed tags. All entries are listed in full lowercase.
*/
const TAG_BLACKLIST = [
public const TAG_BLACKLIST = [
'de' => [
'andere',
'anderes',
@ -32,16 +32,36 @@ final class NodaBlacklistedTerms {
'noch nicht bestimmte objekte',
'ding',
'dinge',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'nichtmünzliches',
'unbestimmt',
'AA',
'BB',
'CC',
'DD',
'EE',
'FF',
'GG',
'HH',
'LL',
'-',
'?',
],
'en' => [
'other',
'others',
'unknown',
'various',
'-',
'?',
],
'hu' => [
'ism.',
'ismeretlen',
'-',
'?',
],
];
@ -122,4 +142,31 @@ final class NodaBlacklistedTerms {
return true;
}
/**
* Checks if a time name is blacklisted in the DB.
*
* @param MDMysqli $mysqli DB connection.
* @param string $lang The user's currently used language.
* @param string $name The name entered by the user.
*
* @return boolean
*/
public static function checkTimeBlacklistedInDb(MDMysqli $mysqli, string $lang, string $name):bool {
$result = $mysqli->query_by_stmt("SELECT 1
FROM `" . DATABASENAME_NODA . "`.`zeiten_blacklist`
WHERE `language` = ?
AND `zeit_name` = ?
LIMIT 1", "ss", $lang, $name);
if ($result->num_rows === 0) {
$result->close();
return false;
}
$result->close();
return true;
}
}

View File

@ -0,0 +1,67 @@
<?PHP
/**
* Abstract class to be inherited by classes for writing consolidated vocabulary names.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Abstract class to be inherited by classes for writing consolidated vocabulary names.
*/
abstract class NodaConsolidatedNamesAbstract {
/**
* This function sanitizes a string.
*
* @param string $inputString Input string.
*
* @return string
*/
final protected static function _sanitizeInputStringStatic(string $inputString):string {
$string = trim($inputString, "; \t" . PHP_EOL);
$string = strtr($string, ["<" => "[", ">" => "]", "\t" => " ", '\n' => ' ',
'<br />' => ' ', '<br/>' => ' ', '<br>' => ' ',
"&lt;br /&gt;" => ' ', '§' => '"'
]);
$string = str_replace(PHP_EOL, ' ', $string);
while (strpos($string, " ") !== false) {
$string = str_replace(" ", " ", $string);
}
$string = strip_tags((string)$string);
return trim(trim($string), ',| ');
}
/**
* Does general cleanup for vocabulary entries.
*
* @param string $input Input string.
*
* @return string
*/
final public static function sanitizeInputString(string $input):string {
$output = strtr(
self::_sanitizeInputStringStatic($input),
[
'<' => '(',
'>' => ')',
'[' => '(',
']' => ')',
"unbekannt" => "",
],
);
// If the first and last character of the name are brackets, remove those.
if (substr($output, 0, 1) === '(' && substr($output, -1) === ')') {
$output = trim($output, '()');
}
return $output;
}
}

View File

@ -0,0 +1,124 @@
<?PHP
/**
* Gathers functions for setting uniform actor names.
*/
declare(strict_types = 1);
/**
* Gathers functions for setting uniform actor names.
*/
final class NodaConsolidatedNamesForPersinst extends NodaConsolidatedNamesAbstract {
/**
* Substrings of an actor name listed as a key in this array will be replaced
* by the corresponding value.
*/
private const _NAME_SANITIZATIONS = [
"mythologische Figur" => "Mythologie",
"Mythologische Figur" => "Mythologie",
"Mythologische Gestalt" => "Mythologie",
"()" => "",
];
/**
* Replaces last characters of a string if $from matches the end of the string,
*
* @param string $from Replace from.
* @param string $to Replace to.
* @param string $name Input name.
*
* @return string
*/
private static function _replaceFromEnd(string $from, string $to, string $name):string {
$length = mb_strlen($from);
if (str_ends_with($name, $from) === true && substr($name, -1 * $length - 1, 1) !== '.') {
$name = str_replace(" ", " ", substr($name, 0, -1 * $length) . $to);
}
return $name;
}
/**
* Cleans and consolidates name parts appearing regularly in German names
* that have a default writing in md.
*
* @param string $name Name of an actor.
*
* @return string
*/
private static function _clean_german_abbreviations(string $name):string {
$name = self::_replaceFromEnd(" d.Ä.", " (der Ältere)", $name);
$name = self::_replaceFromEnd(" d. Ä.", " (der Ältere)", $name);
$name = self::_replaceFromEnd(" (d.Ä.)", " (der Ältere)", $name);
$name = self::_replaceFromEnd(" (d. Ä.)", " (der Ältere)", $name);
$name = self::_replaceFromEnd(" d.J.", " (der Jüngere)", $name);
$name = self::_replaceFromEnd(" d. J.", " (der Jüngere)", $name);
$name = self::_replaceFromEnd(" (d.J.)", " (der Jüngere)", $name);
$name = self::_replaceFromEnd(" (d. J.)", " (der Jüngere)", $name);
return $name;
}
/**
* Tries to make sense of life dates in brackets at the end of an actor's name.
*
* @param string $name Input name.
*
* @return array{name: string, birth: string, death: string}|array{}
*/
public static function parse_life_dates_from_name(string $name):array {
if (str_contains($name, "(") === false || str_ends_with($name, ")") === false) return [];
$parts = explode("(", $name);
if (count($parts) !== 2) return [];
$nameOnly = trim($parts[0]);
$dateString = trim(rtrim($parts[1], ')')); //
if (!empty($dates = NodaTimeSplitter::is_timespan($dateString))
&& $dates->start_year !== '?'
&& $dates->end_year !== '?'
&& $dates->start_year !== $dates->end_year
&& intval($dates->end_year) - intval($dates->start_year) < 150
) {
return [
'name' => $nameOnly,
'birth' => $dates->start_year,
'death' => $dates->end_year,
];
}
return [];
}
/**
* Cleans a persinst name by trimming etc. Also removes uncertainty indicators.
*
* @param string $lang Instance language.
* @param string $persinst_name Input string to clean.
*
* @return string
*/
public static function consolidate_name(string $lang, string $persinst_name):string {
// Run basic replacements
$name = \strtr(self::sanitizeInputString($persinst_name),
self::_NAME_SANITIZATIONS);
$name = NodaUncertaintyHelper::cleanUncertaintyIndicatorsPersinst($name);
if (mb_strlen($name) > 10 && $lang === 'de') {
$name = self::_clean_german_abbreviations($name);
}
// If the persinst name is empty, unset persinst ID
return \trim($name, " ;.\t" . PHP_EOL);
}
}

View File

@ -0,0 +1,506 @@
<?PHP
/**
* Gathers functions for setting uniform place names.
*/
declare(strict_types = 1);
/**
* Gathers functions for setting uniform place names.
*/
final class NodaConsolidatedNamesForPlaces extends NodaConsolidatedNamesAbstract {
/**
* Substrings of an place name listed as a key in this array will be replaced
* by the corresponding value.
*/
private const _NAME_SANITIZATIONS = [
" - " => "-",
"unbekannt" => "",
"Unbekannt" => "",
"unknown" => "",
"Unknown" => "",
];
/** Blacklist for comparison with country names */
private const _COUNTRY_REWRITE_BLACKLISTED_TERMS = [
'District',
'Distrikt',
'India',
'Indien',
'Insel',
'Inseln',
'Tal',
'Yue',
];
private const _PLACE_TYPE_INDICATORS_GERMAN = [
'Insel',
'Stadt',
];
// Indicators signifying that a place is likely subordinate to the other
// if two places are provided in a comma-separated list
private const _PLACE_NARROWER_LOCATION_INDICATORS_GERMAN = [
'gasse',
'straße',
' Straße',
];
// Indicators signifying that a place is likely subordinate to the other
// if two places are provided in a comma-separated list
private const _PLACE_NARROWER_LOCATION_INDICATORS_HUNGARIAN = [
' körut ',
' utca ',
' út ',
];
private const _RELEVANT_ROMAN_NUMERALS = [
'I' => '1',
'II' => '2',
'III' => '3',
'IV' => '4',
'V' => '5',
'VI' => '6',
'VII' => '7',
'VIII' => '8',
'IX' => '9',
'X' => '10',
'XI' => '11',
'XII' => '12',
'XIII' => '13',
'XIV' => '14',
'XV' => '15',
'XVI' => '16',
'XVII' => '17',
'XVIII' => '18',
'XIX' => '19',
'XX' => '20',
];
/**
* @var array<string, list<string>>
*/
private static $_placeNameListCaches = [];
/**
* Rewrites indicators for narrower locations paired with a superordinate location
* into the format "Narrower (Broader)".
* E.g.: "Adalbrechtstr. 12, Berlin" > Adalbrechtstraße 12 (Berlin).
*
* @param string $name Name in which to rewrite.
* @param string $indicator Indicator for narrower place. E.g. "straße".
* @param string $separator Separating character between narrower and broader, e.g. ', '.
*
* @return string
*/
private static function _rewrite_narrower_broader_pairs_to_brackets(string $name, string $indicator, string $separator = ', '):string {
if (str_contains($name, $indicator)
&& substr_count($name, $indicator) === 1
&& substr_count($name, $separator) === 1
&& !str_contains($name, "(")
) {
$parts = explode(', ', $name);
// Skip entries like "Vaci utca 12 Budapest, Vaci utca"
$indicatorTrimmed = trim($indicator);
if ((str_ends_with($parts[0], $indicatorTrimmed) && str_contains($parts[1], $indicatorTrimmed))
|| (str_ends_with($parts[1], $indicatorTrimmed) && str_contains($parts[0], $indicatorTrimmed))
) {
return $name;
}
// Prevent errors in case of "Adalbrechtstraße 12, "
if (!empty($parts[0]) && !empty($parts[1])) {
if (str_contains($parts[0], $indicator)) { // Adalberthstraße 12, Berlin
$street = $parts[0];
$town = $parts[1];
}
else { // Berlin, Adalberthstraße 12
$street = $parts[1];
$town = $parts[0];
}
// Prevent rewrites in cases like "Deák Ferenc utca 16-18. Budapest, V."
if (str_contains($town, '.')) {
return $name;
}
return $street . ' (' . $town . ')';
}
}
return $name;
}
/**
* Cleans and consolidates name parts appearing regularly in German place names.
*
* @param string $name Name of a place.
*
* @return string
*/
private static function _clean_german_abbreviations(string $name):string {
// ABC, Inseln > ABC (Inseln)
foreach (self::_PLACE_TYPE_INDICATORS_GERMAN as $indicator) {
if (str_ends_with($name, ', ' . $indicator)) {
$name = str_replace(', ' . $indicator, ' (' . $indicator . ')', $name);
}
}
// Adalbrechtstr. 12 > Adalbrechtstraße 12
if (str_contains($name, "str. ") && \preg_match("/[a-zA-Z]str. [0-9]/", $name)) {
$name = str_replace("str. ", "straße ", $name);
}
// "Adalbrechtstraße. 12, Berlin" > Adalbrechtstraße 12 (Berlin)
foreach (self::_PLACE_NARROWER_LOCATION_INDICATORS_GERMAN as $indicator) {
$name = self::_rewrite_narrower_broader_pairs_to_brackets($name, $indicator, ', ');
}
return $name;
}
/**
* Cleans and consolidates name parts appearing regularly in Hungarian place names.
*
* @param string $name Name of a place.
*
* @return string
*/
private static function _clean_hungarian_abbreviations(string $name):string {
if (str_contains($name, " krt. ") && \preg_match("/\ krt\.\ [0-9]/", $name)) {
$name = str_replace(" krt. ", " körut ", $name);
}
if (str_contains($name, " u. ") && \preg_match("/\ u\.\ [0-9]/", $name)) {
$name = str_replace(" u. ", " utca ", $name);
}
if (str_contains($name, " ucca ") && \preg_match("/\ ucca\ [0-9]/", $name)) {
$name = str_replace(" ucca ", " utca ", $name);
}
if (str_contains($name, " utcza ") && \preg_match("/\ utcza\ [0-9]/", $name)) {
$name = str_replace(" utcza ", " utca ", $name);
}
if (str_contains($name, " rkp. ") && \preg_match("/\ rkp\.\ [0-9]/", $name)) {
$name = str_replace(" rkp. ", " rakpart ", $name);
}
// "Adalbrecht utca. 12, Berlin" > Adalbrecht utca 12 (Berlin)
foreach (self::_PLACE_NARROWER_LOCATION_INDICATORS_HUNGARIAN as $indicator) {
$name = self::_rewrite_narrower_broader_pairs_to_brackets($name, $indicator, ', ');
}
if (str_contains($name, 'Budapest') && substr_count($name, 'Budapest') === 1) {
foreach(self::_RELEVANT_ROMAN_NUMERALS as $roman_numeral => $arabic) {
$to_match = ' Budapest, ' . $roman_numeral . '.';
if (str_ends_with($name, $to_match)) {
$name = str_replace($to_match, ' (Budapest, ' . $arabic . '. kerület)', $name);
}
}
}
return $name;
}
/**
* Rewrites a Ukrainian language name based on abbreviations explaining the
* hierarchy of named places.
*
* @param string $name Input name to rewrite.
*
* @return string
*/
private static function _rewrite_ukrainian_names_by_hierarchy(string $name):string {
$identifiersByLevel = [
'state' => [' РСР', 'РСР ', ' АРСР', 'АРСР ', ' губернія', 'губернія '],
'oblast' => ['обл.', 'область', 'області', 'округа', 'губернії'],
'region' => ['р-н', 'район'],
'county' => ['повіт'],
'city' => ['м.', 'м '],
'parish' => ['волость'],
'village' => ['смт', 'сільська', 'с. ', 'село'],
'district' => [], // Is also р-н; which it is is determined based on position
'street' => ['вул. '],
];
$levels = [
'country' => '',
'state' => '',
'oblast' => '',
'region' => '',
'county' => '',
'city' => '',
'parish' => '',
'village' => '',
'district' => '',
'street' => '',
];
$parts = explode(',', $name);
foreach ($parts as $part) {
$part = trim($part);
foreach ($identifiersByLevel as $level => $identifiers) {
foreach ($identifiers as $identifier) {
if (str_starts_with($part, $identifier) || str_ends_with($part, $identifier)) {
// Special case: Region can both be rajon or a district within a city
// If both oblast and city are already known, the region will be a
// district within the city.
// Otherwise, it is to be assumed that it is a super-city region.
if ($level === 'region' && !empty($levels['oblast'])
&& (!empty($levels['city']) || !empty($levels['village']))
) {
$level = 'district';
}
if (!empty($levels[$level])) {
# throw new Exception("Used the same level (" . $level . ") twice");
return $name;
}
$levels[$level] = $part;
continue 3;
}
}
}
// Special case: Abbreviated SSRs
if (in_array($part, ['УРСР', 'УССР', 'УСРР'], true)) {
$levels['state'] = $part;
continue;
}
// Unspecified part level: Attempt identifying country
if (!isset($countryNames)) {
$countryNames = self::_loadJsonList(__DIR__ . "/../static/countries.uk.json") + self::_loadJsonList(__DIR__ . "/../static/historical_countries.uk.json");
$countryNames[] = 'СРСР';
$countryNames[] = 'УНР';
$countryNames[] = 'Російська імперія';
$countryNames[] = 'Рос.імперія';
$countryNames[] = 'Рос.имперія';
$countryNames[] = 'Російська імперія-УНР';
}
if (in_array($part, $countryNames, true)) {
$levels['country'] = $part;
continue;
}
// Unspecified level; return
return $name;
}
$main_name = '';
$specifiers = [];
foreach (array_reverse($levels) as $level => $partname) {
if (empty($partname)) continue;
if ($level === 'city' || $level === 'village') {
$strtr = [];
foreach ($identifiersByLevel[$level] as $identifier) $strtr[$identifier] = '';
$partname = trim(strtr($partname, $strtr));
}
if (empty($main_name)) {
$main_name = $partname;
}
else {
$specifiers[] = $partname;
}
}
$output = $main_name;
if (!empty($specifiers)) {
$output .= ' (' . implode(', ', $specifiers) . ')';
}
return $output;
}
/**
* Cleans and consolidates name parts appearing regularly in Ukrainian place names.
*
* @param string $name Name of an place.
*
* @return string
*/
private static function _clean_ukrainian_abbreviations(string $name):string {
if (str_contains($name, " р-н,") || str_contains($name, " р") || str_ends_with($name, " р")) {
$name = str_replace(" р", " район", $name);
}
if (str_contains($name, ',')) {
$name = self::_rewrite_ukrainian_names_by_hierarchy($name);
}
return $name;
}
/**
* Loads a JSON file, optionally loading it cached through a private static variable
* if reuse is expectable (= in the case of CLI usage).
*
* @param non-empty-string $filename File name to load.
*
* @return list<string>
*/
private static function _loadJsonList(string $filename):array {
if (PHP_SAPI === 'cli' && isset(self::$_placeNameListCaches[$filename])) {
return self::$_placeNameListCaches[$filename];
}
try {
$output = json_decode(MD_STD::file_get_contents($filename), true);
}
catch (MDFileDoesNotExist $e) {
self::$_placeNameListCaches[$filename] = [];
return [];
}
if ($output === false) {
throw new Exception("Failed to get list");
}
if (PHP_SAPI === 'cli') {
self::$_placeNameListCaches[$filename] = $output;
}
return $output;
}
/**
* Moves names of regions to brackets using pre-generated lists of countries,
* historical country names, etc.
*
* @param string $lang Instance language.
* @param string $name Input string to clean.
*
* @return string
*/
private static function _move_region_names_to_brackets(string $lang, string $name):string {
$separators = ['-', ', '];
foreach ($separators as $separator) {
if (!str_contains($name, $separator) || substr_count($name, $separator) !== 1) continue;
// Get parts and trim them
$parts = explode($separator, $name);
foreach ($parts as $key => $value) {
$parts[$key] = trim($value);
}
// Load place names
$countryNames = self::_loadJsonList(__DIR__ . "/../static/countries.$lang.json") + self::_loadJsonList(__DIR__ . "/../static/historical_countries.$lang.json");
$cardinal_directions = self::_loadJsonList(__DIR__ . "/../static/cardinal_directions.json");
$part0IsCountry = in_array($parts[0], $countryNames, true);
$part1IsCountry = in_array($parts[1], $countryNames, true);
// Skip if the full name is in the list of country names
if (in_array($name, $countryNames, true)) {
return $name;
}
// If one of the parts is a blacklisted term or a cardinal directions, skip this
if ((in_array($parts[0], self::_COUNTRY_REWRITE_BLACKLISTED_TERMS, true)
|| in_array($parts[0], $cardinal_directions, true)
|| in_array(strtolower($parts[0]), $cardinal_directions, true))
|| (in_array($parts[1], self::_COUNTRY_REWRITE_BLACKLISTED_TERMS, true)
|| in_array($parts[1], $cardinal_directions, true)
|| in_array(strtolower($parts[1]), $cardinal_directions, true))
) {
return $name;
}
if ($part0IsCountry === true && $part1IsCountry === false) {
return $parts[1] . ' (' . $parts[0] . ')';
}
else if ($part0IsCountry === false && $part1IsCountry === true) {
return $parts[0] . ' (' . $parts[1] . ')';
}
}
return $name;
}
/**
* Removes duplicates after commas.
*
* @param string $ort_name Place name to clean.
*
* @return string
*/
private static function _remove_duplicates_after_commas(string $ort_name):string {
if (str_contains($ort_name, ',') === false) {
return $ort_name;
}
$parts = explode(', ', $ort_name);
return implode(', ', array_unique($parts));
}
/**
* Cleans a place name by trimming etc. Also removes uncertainty indicators.
*
* @param string $lang Instance language.
* @param string $ort_name Input string to clean.
*
* @return string
*/
public static function consolidate_name(string $lang, string $ort_name):string {
// Run basic replacements
$nameSanitizations = self::_NAME_SANITIZATIONS;
/*
if (substr_count($ort_name, "/") === 1 && !str_contains($ort_name, '.')) {
$nameSanitizations["/"] = "-";
}
*/
$ort_name = strtr(self::sanitizeInputString($ort_name), $nameSanitizations);
$ort_name = self::sanitizeInputString(NodaUncertaintyHelper::cleanUncertaintyIndicatorsPlace($ort_name));
// Remove duplicates after commas
// Västerdås, Schweden, Schweden > Västerdås, Schweden
$ort_name = self::_remove_duplicates_after_commas($ort_name);
$ort_name = match ($lang) {
'de' => self::_clean_german_abbreviations($ort_name),
'hu' => self::_clean_hungarian_abbreviations($ort_name),
'uk' => self::_clean_ukrainian_abbreviations($ort_name),
default => $ort_name,
};
$ort_name = self::_move_region_names_to_brackets($lang, $ort_name);
return $ort_name;
}
}

View File

@ -0,0 +1,74 @@
<?PHP
/**
* Represents a time indicator (CE / BCE).
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Represents a time indicator (CE / BCE).
*/
enum NodaCountingTimeIndicator implements JsonSerializable {
case ce;
case bce;
/**
* Returns a value of this type based on a string.
*
* @param string $input Input to get a value from.
*
* @return NodaCountingTimeIndicator
*/
public static function fromString(string $input):NodaCountingTimeIndicator {
return match($input) {
'+' => self::ce,
'-' => self::bce,
'ce' => self::ce,
'bce' => self::bce,
default => throw new MDpageParameterNotFromListException("Unknown counting time indicator (bc / bce)"),
};
}
/**
* Returns a canonical string representation.
*
* @return string
*/
public function toString():string {
return match($this) {
self::ce => '+',
self::bce => '-',
};
}
/**
* Returns a canonical string representation.
*
* @return string
*/
public function toGerman():string {
return match($this) {
self::ce => ' n. Chr.',
self::bce => ' v. Chr.',
};
}
/**
* Provides the option to serialize as a string during json_encode().
*
* @return string
*/
public function jsonSerialize():string {
return $this->toString();
}
}

View File

@ -26,7 +26,13 @@ final class NodaDbAdmin {
`language` string,
`name` text,
`description` text,
`timestamp` timestamp) min_infix_len = '3'");
`timestamp` timestamp)
min_infix_len = '3'
charset_table='" . MDMysqli::MANTICORE_CHARSET_TABLE . "'
expansion_limit = '32'
blend_chars = '" . MDMysqliSetup::DEFAULT_MANTICORE_BLEND_CHARS . "'
blend_mode = '" . MDMysqliSetup::DEFAULT_MANTICORE_BLEND_MODE . "'
rt_mem_limit = '2G'");
MDConsole::write("Create table `" . $index_name . "`");
}
@ -46,7 +52,13 @@ final class NodaDbAdmin {
`language` string,
`name` text,
`description` text,
`timestamp` timestamp) min_infix_len = '3'");
`timestamp` timestamp)
min_infix_len = '3'
expansion_limit = '32'
blend_chars = '" . MDMysqliSetup::DEFAULT_MANTICORE_BLEND_CHARS . "'
blend_mode = '" . MDMysqliSetup::DEFAULT_MANTICORE_BLEND_MODE . "'
charset_table='" . MDMysqli::MANTICORE_CHARSET_TABLE . "'
rt_mem_limit = '2G'");
MDConsole::write("Create table `" . $index_name . "`");
}

View File

@ -0,0 +1,42 @@
<?PHP
/**
* Checks the distinctly_typed_strings table for whether it
* contains a string.
*/
declare(strict_types = 1);
/**
* Checks the distinctly_typed_strings table for whether it
* contains a string.
*/
final class NodaDistinctlyTypedStrings {
/**
* Checks the vocabulary database whether it contains a given string.
*
* @param MDMysqli $mysqli_noda DB connection.
* @param string $lang Language to check in.
* @param string $search_term Search term.
*
* @return 'persinst'|'zeiten'|'orte'|'tag'|''
*/
public static function lookup(MDMysqli $mysqli_noda, string $lang, string $search_term):string {
$result = $mysqli_noda->query_by_stmt("SELECT `type`
FROM `distinctly_typed_strings`
WHERE `input_string` = ?
AND `language` = ?", "ss", $search_term, $lang);
if (!($cur = $result->fetch_row())) {
$result->close();
return '';
}
$result->close();
if (!in_array($cur[0], ['persinst', 'zeiten', 'orte', 'tag', ''], true)) {
throw new Exception("Distinctly typed string of unknown type: " . $cur[0]);
}
return $cur[0];
}
}

322
src/NodaGroup.php Normal file
View File

@ -0,0 +1,322 @@
<?PHP
/**
* Manages vocabulary groups.
*
* @file
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Manages vocabulary groups.
*/
final class NodaGroup {
private MDMysqli $_mysqli_noda;
/**
* Lists all active groups.
*
* @param integer $limit Limit.
* @param integer $offset Offset.
*
* @return array<array{id: int, name: string}>
*/
public function list(int $limit = 50, int $offset = 0):array {
$output = [];
$result = $this->_mysqli_noda->query_by_stmt("SELECT `group_id`, `group_name`
FROM `group`
ORDER BY `group_id` DESC
LIMIT ?
OFFSET ?", "ii", $limit, $offset);
while ($cur = $result->fetch_row()) {
$output[] = [
'id' => (int)$cur[0],
'name' => $cur[1],
];
}
$result->close();
return $output;
}
/**
* Returns basic description of a group.
*
* @param integer $group_id Group ID.
*
* @return array{name: string, comment: string}
*/
public function getDescription(int $group_id):array {
$result = $this->_mysqli_noda->query_by_stmt("SELECT `group_name`, `comment`
FROM `group`
WHERE `group_id` = ?", "i", $group_id);
if (!($cur = $result->fetch_row())) {
$result->close();
throw new MDmainEntityNotExistentException("This group does not seem to exist");
}
$result->close();
return [
'name' => $cur[0],
'comment' => $cur[1],
];
}
/**
* Adds a group.
*
* @param string $name Name of the group.
* @param string $comment Comment / note on the group.
*
* @return integer
*/
public function insert(string $name, string $comment = ''):int {
if (empty($name)) {
throw new MDpageParameterMissingException("Name cannot be empty when adding groups.");
}
$insertStmt = $this->_mysqli_noda->do_prepare("INSERT INTO `group`
(`group_name`, `comment`)
VALUES
(?, ?)");
$insertStmt->bind_param("ss", $name, $comment);
$insertStmt->execute();
$output = $insertStmt->get_insert_id();
$insertStmt->close();
return $output;
}
/**
* Updates a group.
*
* @param integer $group_id ID of the group to update.
* @param string $name Name of the group.
* @param string $comment Optional: Comment for the group.
*
* @return void
*/
public function update(int $group_id, string $name, string $comment = ''):void {
if (empty($name)) {
throw new MDpageParameterMissingException("Name cannot be empty when adding groups.");
}
$insertStmt = $this->_mysqli_noda->do_prepare("UPDATE `group`
SET `group_name` = ?,
`comment` = ?
WHERE `group_id` = ?");
$insertStmt->bind_param("ssi", $name, $comment, $group_id);
$insertStmt->execute();
$insertStmt->close();
}
/**
* Deletes a group.
*
* @param integer $group_id ID of the group to delete.
*
* @return void
*/
public function delete(int $group_id):void {
$this->_mysqli_noda->update_query_by_stmt("DELETE FROM `v_group_persinst`
WHERE `group_id` = ?", "i", $group_id);
$this->_mysqli_noda->update_query_by_stmt("DELETE FROM `v_group_orte`
WHERE `group_id` = ?", "i", $group_id);
$this->_mysqli_noda->update_query_by_stmt("DELETE FROM `v_group_zeiten`
WHERE `group_id` = ?", "i", $group_id);
$this->_mysqli_noda->update_query_by_stmt("DELETE FROM `v_group_tag`
WHERE `group_id` = ?", "i", $group_id);
$this->_mysqli_noda->update_query_by_stmt("DELETE FROM `group`
WHERE `group_id` = ?", "i", $group_id);
}
/**
* Adds actors to a nodac group.
*
* @param integer $group_id Group ID.
* @param array<integer> $input_ids List of entries to link to the group.
*
* @return void
*/
public function linkActors(int $group_id, array $input_ids):void {
if (empty($input_ids = MD_STD_IN::sanitize_id_array($input_ids))) return;
// Check which entries actually exist
$idsToLink = [];
$result = $this->_mysqli_noda->do_read_query("SELECT `persinst_id`
FROM `persinst`
WHERE `persinst_id` IN (" . $this->_mysqli_noda->escape_in($input_ids) . ")");
while ($cur = $result->fetch_row()) {
$idsToLink[] = (int)$cur[0];
}
$result->close();
$this->_mysqli_noda->autocommit(false);
$linkStmt = $this->_mysqli_noda->do_prepare("INSERT INTO `v_group_persinst`
(`group_id`, `persinst_id`)
VALUES (?, ?)");
foreach ($idsToLink as $id) {
$linkStmt->bind_param("ii", $group_id, $id);
$linkStmt->execute();
}
$linkStmt->close();
$this->_mysqli_noda->commit();
$this->_mysqli_noda->autocommit(true);
}
/**
* Adds places to a nodac group.
*
* @param integer $group_id Group ID.
* @param array<integer> $input_ids List of entries to link to the group.
*
* @return void
*/
public function linkPlaces(int $group_id, array $input_ids):void {
if (empty($input_ids = MD_STD_IN::sanitize_id_array($input_ids))) return;
// Check which entries actually exist
$idsToLink = [];
$result = $this->_mysqli_noda->do_read_query("SELECT `ort_id`
FROM `orte`
WHERE `ort_id` IN (" . $this->_mysqli_noda->escape_in($input_ids) . ")");
while ($cur = $result->fetch_row()) {
$idsToLink[] = (int)$cur[0];
}
$result->close();
$this->_mysqli_noda->autocommit(false);
$linkStmt = $this->_mysqli_noda->do_prepare("INSERT INTO `v_group_orte`
(`group_id`, `ort_id`)
VALUES (?, ?)");
foreach ($idsToLink as $id) {
$linkStmt->bind_param("ii", $group_id, $id);
$linkStmt->execute();
}
$linkStmt->close();
$this->_mysqli_noda->commit();
$this->_mysqli_noda->autocommit(true);
}
/**
* Adds times to a nodac group.
*
* @param integer $group_id Group ID.
* @param array<integer> $input_ids List of entries to link to the group.
*
* @return void
*/
public function linkTimes(int $group_id, array $input_ids):void {
if (empty($input_ids = MD_STD_IN::sanitize_id_array($input_ids))) return;
// Check which entries actually exist
$idsToLink = [];
$result = $this->_mysqli_noda->do_read_query("SELECT `zeit_id`
FROM `zeiten`
WHERE `zeit_id` IN (" . $this->_mysqli_noda->escape_in($input_ids) . ")");
while ($cur = $result->fetch_row()) {
$idsToLink[] = (int)$cur[0];
}
$result->close();
$this->_mysqli_noda->autocommit(false);
$linkStmt = $this->_mysqli_noda->do_prepare("INSERT INTO `v_group_zeiten`
(`group_id`, `zeit_id`)
VALUES (?, ?)");
foreach ($idsToLink as $id) {
$linkStmt->bind_param("ii", $group_id, $id);
$linkStmt->execute();
}
$linkStmt->close();
$this->_mysqli_noda->commit();
$this->_mysqli_noda->autocommit(true);
}
/**
* Adds tags to a nodac group.
*
* @param integer $group_id Group ID.
* @param array<integer> $input_ids List of entries to link to the group.
*
* @return void
*/
public function linkTags(int $group_id, array $input_ids):void {
if (empty($input_ids = MD_STD_IN::sanitize_id_array($input_ids))) return;
// Check which entries actually exist
$idsToLink = [];
$result = $this->_mysqli_noda->do_read_query("SELECT `tag_id`
FROM `tag`
WHERE `tag_id` IN (" . $this->_mysqli_noda->escape_in($input_ids) . ")");
while ($cur = $result->fetch_row()) {
$idsToLink[] = (int)$cur[0];
}
$result->close();
$this->_mysqli_noda->autocommit(false);
$linkStmt = $this->_mysqli_noda->do_prepare("INSERT INTO `v_group_tag`
(`group_id`, `tag_id`)
VALUES (?, ?)");
foreach ($idsToLink as $id) {
$linkStmt->bind_param("ii", $group_id, $id);
$linkStmt->execute();
}
$linkStmt->close();
$this->_mysqli_noda->commit();
$this->_mysqli_noda->autocommit(true);
}
/**
* Constructor.
*
* @param MDMysqli $mysqli_noda DB connection.
*
* @return void
*/
public function __construct(MDMysqli $mysqli_noda) {
$this->_mysqli_noda = $mysqli_noda;
}
}

View File

@ -10,6 +10,30 @@ declare(strict_types = 1);
* Contains static functions for getting IDs for noda entries by various means.
*/
final class NodaIDGetter {
/**
* Comparison operation that allows for equality but also returns true
* if capitalization differs. If diacritics are available in one case
* and not in the other, there will be no match.
* This is needed to identify sufficiently exact matches despite a table
* collation of *_ci in MySQL.
*
* @param string $string_one First string in comparison.
* @param string $string_two Second string in comparison.
*
* @return boolean
*/
private static function _stri_matches(string $string_one, string $string_two):bool {
if ($string_one === $string_two) {
return true;
}
if (\strtolower($string_one) === \strtolower($string_two)) {
return true;
}
return false;
}
//
// Actors
@ -40,6 +64,33 @@ final class NodaIDGetter {
}
/**
* Returns persinst ID by name, checking the different options for it.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $lang Language to check in.
* @param string $name Name of the persinst to search for.
* @param string $birth Birth year.
* @param string $death Death year.
*
* @return integer
*/
public static function getPersinstIDByNamePlusYears(MDMysqli $mysqli_noda, string $lang, string $name, string $birth, string $death):int {
if (empty($name)) return 0;
if ($persinstByTransName = self::getPersinstIDByTransNamePlusYears($mysqli_noda, $lang, $name, $birth, $death)) {
return $persinstByTransName;
}
if ($persinstByBaseName = self::getPersinstIDByBaseNamePlusYears($mysqli_noda, $name, $birth, $death)) {
return $persinstByBaseName;
}
return 0;
}
/**
* Returns persinst ID by entry in persinst name rewriting table.
*
@ -53,21 +104,22 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$persinstRewriteResult = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`, `input_name`
FROM `persinst_rewriting`
WHERE `language` = ?
AND `input_name` = ?
LIMIT 1", "ss", $lang, $name);
if ($persinstRewriteData = $persinstRewriteResult->fetch_row()) {
$output = $persinstRewriteData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$persinstRewriteResult->close();
return $output;
return 0;
}
@ -84,21 +136,91 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$persinstByTLNameResult = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`, `trans_name`
FROM `persinst_translation`
WHERE `trans_name` = ?
AND `trans_language` = ?
LIMIT 2", "ss", $name, $lang);
if ($persinstByTlData = $persinstByTLNameResult->fetch_row()) {
$output = $persinstByTlData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$persinstByTLNameResult->close();
return 0;
return $output;
}
/**
* Returns persinst ID by entry in persinst translations table,
* irrespective of language.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $name Name of the persinst to search for.
*
* @return integer
*/
public static function getPersinstIDByAnyTransName(MDMysqli $mysqli_noda, string $name):int {
if (empty($name)) return 0;
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`, `trans_name`
FROM `persinst_translation`
WHERE `trans_name` = ?
LIMIT 2", "s", $name);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
/**
* Returns persinst ID by entry in persinst translations table
* plus birth and death.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $lang Language to check in.
* @param string $name Name of the persinst to search for.
* @param string $birth Birth year.
* @param string $death Death year.
*
* @return integer
*/
public static function getPersinstIDByTransNamePlusYears(MDMysqli $mysqli_noda, string $lang, string $name, string $birth, string $death):int {
if (empty($name)) return 0;
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_translation`.`persinst_id`, `trans_name`
FROM `persinst_translation`, `persinst`
WHERE `persinst_translation`.`persinst_id` = `persinst`.`persinst_id`
AND `trans_name` = ?
AND `trans_language` = ?
AND `persinst_geburtsjahr` = ?
AND `persinst_sterbejahr` = ?
LIMIT 2", "ssss", $name, $lang, $birth, $death);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
@ -114,22 +236,61 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$persinstByBaseNameResult = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`, `persinst_anzeigename`, `persinst_name`, CONCAT(`persinst_name`, ' (', `persinst_geburtsjahr`, '-', `persinst_sterbejahr`, ')')
FROM `persinst`
WHERE `persinst_anzeigename` = ?
OR `persinst_name` = ?
OR CONCAT(`persinst_name`, ' (', `persinst_geburtsjahr`, '-', `persinst_sterbejahr`, ')') = ?
LIMIT 2", "sss", $name, $name, $name);
if ($persinstByBaseData = $persinstByBaseNameResult->fetch_row()) {
$output = $persinstByBaseData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name) || self::_stri_matches($cur[2], $name) || self::_stri_matches($cur[3], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$persinstByBaseNameResult->close();
return 0;
return $output;
}
/**
* Returns persinst ID by base name.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $name Name of the persinst to search for.
* @param string $birth Birth year.
* @param string $death Death year.
*
* @return integer
*/
public static function getPersinstIDByBaseNamePlusYears(MDMysqli $mysqli_noda, string $name, string $birth, string $death):int {
if (empty($name)) return 0;
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`, `persinst_anzeigename`, `persinst_name`, CONCAT(`persinst_name`, ' (', `persinst_geburtsjahr`, '-', `persinst_sterbejahr`, ')')
FROM `persinst`
WHERE (
`persinst_anzeigename` = ?
OR `persinst_name` = ?
OR CONCAT(`persinst_name`, ' (', `persinst_geburtsjahr`, '-', `persinst_sterbejahr`, ')') = ?
)
AND `persinst_geburtsjahr` = ?
AND `persinst_sterbejahr` = ?
LIMIT 2", "sssss", $name, $name, $name, $birth, $death);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name) || self::_stri_matches($cur[2], $name) || self::_stri_matches($cur[3], $name)) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
@ -183,22 +344,23 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$lookUpName = $name . $birthYear . $deathYear;
$persinstByImportLogResult = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `persinst_id`, `input_string`
FROM `persinst_logged_imports`
WHERE `instance` = ?
AND `institution_id` = ?
AND `input_string` = ?
LIMIT 2", "sis", $instance, $institution_id, $lookUpName);
if ($persinstByImportLogData = $persinstByImportLogResult->fetch_row()) {
$output = $persinstByImportLogData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $lookUpName)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$persinstByImportLogResult->close();
return $output;
return 0;
}
@ -244,21 +406,22 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$placeRewriteResult = $mysqli_noda->query_by_stmt("
SELECT `ort_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `ort_id`, `input_name`
FROM `ort_rewriting`
WHERE `language` = ?
AND `input_name` = ?
LIMIT 1", "ss", $lang, $name);
if ($placeRewriteData = $placeRewriteResult->fetch_row()) {
$output = $placeRewriteData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$placeRewriteResult->close();
return $output;
return 0;
}
@ -274,20 +437,21 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$placeByBaseNameResult = $mysqli_noda->query_by_stmt("
SELECT `ort_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `ort_id`, `ort_name`
FROM `orte`
WHERE `ort_name` = ?
LIMIT 2", "s", $name);
if ($placeByBaseData = $placeByBaseNameResult->fetch_row()) {
$output = $placeByBaseData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$placeByBaseNameResult->close();
return $output;
return 0;
}
@ -304,21 +468,53 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$placeByTLNameResult = $mysqli_noda->query_by_stmt("
SELECT `ort_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `ort_id`, `trans_name`
FROM `ort_translation`
WHERE `trans_name` = ?
AND `trans_language` = ?
LIMIT 2", "ss", $name, $lang);
if ($placeByTlData = $placeByTLNameResult->fetch_row()) {
$output = $placeByTlData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$placeByTLNameResult->close();
return 0;
return $output;
}
/**
* Returns place ID by entry in place translations table, irrespective of
* language.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $name Name of the place to search for.
*
* @return integer
*/
public static function getPlaceIDByAnyTransName(MDMysqli $mysqli_noda, string $name):int {
if (empty($name)) return 0;
$result = $mysqli_noda->query_by_stmt("
SELECT `ort_id`, `trans_name`
FROM `ort_translation`
WHERE `trans_name` = ?
LIMIT 2", "s", $name);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
@ -369,22 +565,23 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$placeByImportLogResult = $mysqli_noda->query_by_stmt("
SELECT `ort_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `ort_id`, `input_string`
FROM `orte_logged_imports`
WHERE `instance` = ?
AND `institution_id` = ?
AND `input_string` = ?
LIMIT 2", "sis", $instance, $institution_id, $name);
if ($placeByImportLogData = $placeByImportLogResult->fetch_row()) {
$output = $placeByImportLogData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($cur[1], $name)) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$placeByImportLogResult->close();
return $output;
return 0;
}
@ -432,17 +629,19 @@ final class NodaIDGetter {
$output = [];
$tagRewriteResult = $mysqli_noda->query_by_stmt("
SELECT `tag_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `tag_id`, `input_name`
FROM `tag_rewriting`
WHERE `tag_language` = ?
AND `input_name` = ?", "ss", $lang, $name);
while ($tagRewriteData = $tagRewriteResult->fetch_row()) {
$output[] = $tagRewriteData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$output[] = $cur[0];
}
}
$tagRewriteResult->close();
$result->close();
return $output;
@ -460,20 +659,21 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$tagByBaseNameResult = $mysqli_noda->query_by_stmt("
SELECT `tag_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `tag_id`, `tag_name`
FROM `tag`
WHERE `tag_name` = ?
LIMIT 2", "s", $name);
if ($tagByBaseData = $tagByBaseNameResult->fetch_row()) {
$output = $tagByBaseData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$tagByBaseNameResult->close();
return $output;
return 0;
}
@ -490,21 +690,53 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$tagByTLNameResult = $mysqli_noda->query_by_stmt("
SELECT `tag_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `tag_id`, `trans_name`
FROM `tag_translation`
WHERE `trans_name` = ?
AND `trans_language` = ?
LIMIT 2", "ss", $name, $lang);
if ($tagByTlData = $tagByTLNameResult->fetch_row()) {
$output = $tagByTlData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$tagByTLNameResult->close();
return 0;
return $output;
}
/**
* Returns tag ID by entry in tag translations table,
* irrespective of language.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $name Name of the tag to search for.
*
* @return integer
*/
public static function getTagIDByAnyTransName(MDMysqli $mysqli_noda, string $name):int {
if (empty($name)) return 0;
$result = $mysqli_noda->query_by_stmt("
SELECT `tag_id`, `trans_name`
FROM `tag_translation`
WHERE `trans_name` = ?
LIMIT 2", "s", $name);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
@ -555,22 +787,23 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$tagByImportLogResult = $mysqli_noda->query_by_stmt("
SELECT `tag_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `tag_id`, `input_string`
FROM `tag_logged_imports`
WHERE `instance` = ?
AND `institution_id` = ?
AND `input_string` = ?
LIMIT 2", "sis", $instance, $institution_id, $name);
if ($tagByImportLogData = $tagByImportLogResult->fetch_row()) {
$output = $tagByImportLogData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$tagByImportLogResult->close();
return $output;
return 0;
}
@ -603,6 +836,39 @@ final class NodaIDGetter {
}
/**
* Returns time ID by entry in time name rewriting table.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $lang Language to check in.
* @param string $name Name of the time to search for.
*
* @return integer
*/
public static function getTimeIDByRewrite(MDMysqli $mysqli_noda, string $lang, string $name):int {
if (empty($name)) return 0;
$output = [];
$result = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`, `input_name`
FROM `zeit_rewriting`
WHERE `language` = ?
AND `input_name` = ?", "ss", $lang, $name);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
/**
* Returns time ID by base name.
*
@ -615,20 +881,21 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$timeByBaseNameResult = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`, `zeit_name`
FROM `zeiten`
WHERE `zeit_name` = ?
LIMIT 2", "s", $name);
if ($timeByBaseData = $timeByBaseNameResult->fetch_row()) {
$output = $timeByBaseData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$timeByBaseNameResult->close();
return $output;
return 0;
}
@ -645,21 +912,52 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$timeByTLNameResult = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`, `trans_name`
FROM `zeit_translation`
WHERE `trans_name` = ?
AND `trans_language` = ?
LIMIT 2", "ss", $name, $lang);
if ($timeByTlData = $timeByTLNameResult->fetch_row()) {
$output = $timeByTlData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$timeByTLNameResult->close();
return 0;
return $output;
}
/**
* Returns time ID by entry in time translations table.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $name Name of the time to search for.
*
* @return integer
*/
public static function getTimeIDByAnyTransName(MDMysqli $mysqli_noda, string $name):int {
if (empty($name)) return 0;
$result = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`, `trans_name`
FROM `zeit_translation`
WHERE `trans_name` = ?
LIMIT 2", "s", $name);
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
}
$result->close();
return 0;
}
@ -677,22 +975,23 @@ final class NodaIDGetter {
if (empty($name)) return 0;
$timeByImportLogResult = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`
$result = $mysqli_noda->query_by_stmt("
SELECT `zeit_id`, `input_string`
FROM `zeiten_logged_imports`
WHERE `instance` = ?
AND `institution_id` = ?
AND `input_string` = ?
LIMIT 2", "sis", $instance, $institution_id, $name);
if ($timeByImportLogData = $timeByImportLogResult->fetch_row()) {
$output = $timeByImportLogData[0];
while ($cur = $result->fetch_row()) {
if (self::_stri_matches($name, $cur[1])) {
$result->close();
return (int)$cur[0];
}
else $output = 0;
}
$result->close();
$timeByImportLogResult->close();
return $output;
return 0;
}
@ -810,6 +1109,10 @@ final class NodaIDGetter {
return $timeIdByName;
}
if (!empty($timeIdByRewrite = self::getTimeIDByRewrite($mysqli_noda, $lang, $name))) {
return $timeIdByRewrite;
}
if ($instance !== "") {
if (($timeIdByImportLog = self::getTimeIDByImportLog($mysqli_noda, $instance, $institution_id, $name)) !== 0) {
return $timeIdByImportLog;
@ -819,4 +1122,79 @@ final class NodaIDGetter {
return 0;
}
/**
* Checks each string in a list of strings for its existence as a tag name.
*
* @param MDMysqli $mysqli_noda Database connection.
* @param string $lang Language to check in.
* @param non-empty-array<string> $phrases List of phrases to check.
*
* @return array{count: int, tag: integer[], actor: integer[], time: integer[], place: integer[]}
*/
public static function searchEntryNamesByList(MDMysqli $mysqli_noda, string $lang, array $phrases):array {
$output = [
'count' => 0,
'tag' => [],
'actor' => [],
'time' => [],
'place' => [],
];
foreach ($phrases as $phrase) {
if (($tag_id = NodaIDGetter::getTagIDByNamesAndRewrites($mysqli_noda, $lang, $phrase)) !== 0 && !in_array($tag_id, $output['tag'], true)) {
$output['tag'][] = $tag_id;
++$output['count'];
}
else if (($tag_id_by_tl = NodaIDGetter::getTagIDByAnyTransName($mysqli_noda, $phrase)) !== 0 && !in_array($tag_id_by_tl, $output['tag'], true)) {
$output['tag'][] = $tag_id_by_tl;
++$output['count'];
}
else if (($place_id = NodaIDGetter::getPlaceIDByNamesAndRewrites($mysqli_noda, $lang, $phrase)) !== 0 && !in_array($place_id, $output['place'], true)) {
$output['place'][] = $place_id;
++$output['count'];
}
else if (($place_id = NodaIDGetter::getPlaceIDByAnyTransName($mysqli_noda, $phrase)) !== 0 && !in_array($place_id, $output['place'], true)) {
$output['place'][] = $place_id;
++$output['count'];
}
else if (($persinst_id = NodaIDGetter::getPersinstIDByNamesAndRewrites($mysqli_noda, $lang, $phrase, '', '')) !== 0 && !in_array($persinst_id, $output['actor'], true)) {
$output['actor'][] = $persinst_id;
++$output['count'];
}
else if (($persinst_id = NodaIDGetter::getPersinstIDByAnyTransName($mysqli_noda, $phrase)) !== 0 && !in_array($persinst_id, $output['actor'], true)) {
$output['actor'][] = $persinst_id;
++$output['count'];
}
else if (($time_id = NodaIDGetter::getTimeIDByNamesAndRewrites($mysqli_noda, $lang, $phrase)) !== 0 && !in_array($time_id, $output['time'], true)) {
$output['time'][] = $time_id;
++$output['count'];
}
else if (($time_id = NodaIDGetter::getTimeIDByAnyTransName($mysqli_noda, $phrase)) !== 0 && !in_array($time_id, $output['time'], true)) {
$output['time'][] = $time_id;
++$output['count'];
}
}
if (count($phrases) !== $output['count']) {
return [
'count' => 0,
'tag' => [],
'actor' => [],
'time' => [],
'place' => [],
];
}
if (!empty($output['tag'])) sort($output['tag']);
if (!empty($output['actor'])) sort($output['actor']);
if (!empty($output['time'])) sort($output['time']);
if (!empty($output['place'])) sort($output['place']);
return $output;
}
}

View File

@ -32,8 +32,12 @@ final class NodaImportLogger {
$logStmt = $mysqli_noda->do_prepare("INSERT INTO `persinst_logged_imports`
(`instance`, `institution_id`, `input_string`, `persinst_id`)
VALUES (?, ?, ?, ?)");
try {
$logStmt->bind_param("sisi", $instance, $institution_id, $loggedName, $persinst_id);
$logStmt->execute();
}
catch (MDMysqliDuplicateKeysError $e) {
}
$logStmt->close();
}
@ -54,8 +58,12 @@ final class NodaImportLogger {
$logStmt = $mysqli_noda->do_prepare("INSERT INTO `orte_logged_imports`
(`instance`, `institution_id`, `input_string`, `ort_id`)
VALUES (?, ?, ?, ?)");
try {
$logStmt->bind_param("sisi", $instance, $institution_id, $name, $ort_id);
$logStmt->execute();
}
catch (MDMysqliDuplicateKeysError $e) {
}
$logStmt->close();
}
@ -76,8 +84,12 @@ final class NodaImportLogger {
$logStmt = $mysqli_noda->do_prepare("INSERT INTO `zeiten_logged_imports`
(`instance`, `institution_id`, `input_string`, `zeit_id`)
VALUES (?, ?, ?, ?)");
try {
$logStmt->bind_param("sisi", $instance, $institution_id, $name, $zeit_id);
$logStmt->execute();
}
catch (MDMysqliDuplicateKeysError $e) {
}
$logStmt->close();
}
@ -98,8 +110,12 @@ final class NodaImportLogger {
$logStmt = $mysqli_noda->do_prepare("INSERT INTO `tag_logged_imports`
(`instance`, `institution_id`, `input_string`, `tag_id`)
VALUES (?, ?, ?, ?)");
try {
$logStmt->bind_param("sisi", $instance, $institution_id, $name, $tag_id);
$logStmt->execute();
}
catch (MDMysqliDuplicateKeysError $e) {
}
$logStmt->close();
}

View File

@ -19,6 +19,7 @@ final class NodaLogEdit {
const SECTION_WHITELIST = [
'base',
'addition',
'noda_link',
'name_variant',
'logged_import_concordances',
@ -30,6 +31,7 @@ final class NodaLogEdit {
'synchronize',
'status',
'translation',
'group',
];
/**

View File

@ -107,4 +107,26 @@ final class NodaMailChecker {
return self::validateMailDomainAccessibilityCached($mysqli, $domain);
}
/**
* Validates mail address, throwing an exception if the mail address is not valid.
*
* @param MDMysqli $mysqli DB connection.
* @param string $mail_address Address.
*
* @return string
*/
public static function validateMail(MDMysqli $mysqli, string $mail_address):string {
if (empty($mail_address)) return $mail_address;
$mail_address = MD_STD_IN::sanitize_email($mail_address);
if (self::validateMailByDomainAccessibilityCached($mysqli, $mail_address) === false) {
throw new MDInvalidEmail("The host name of the entered mail address is invalid");
}
return $mail_address;
}
}

View File

@ -143,6 +143,54 @@ final class NodaNameGetter {
}
/**
* Static function for getting persinst names in bulk.
*
* @param MDMysqli $mysqli_noda DB connection.
* @param string $lang Language.
* @param array<integer> $persinst_ids Persinst IDs.
*
* @return array<int, string>
*/
public static function getBatchPersinstNamesWithLifeDates(MDMysqli $mysqli_noda, string $lang, array $persinst_ids):array {
$output = [];
// Get translations
$result = $mysqli_noda->do_read_query("SELECT `persinst`.`persinst_id`, `trans_name`, `persinst_geburtsjahr`, `persinst_sterbejahr`
FROM `persinst_translation`, `persinst`
WHERE `trans_language` = '" . $mysqli_noda->escape_string($lang) . "'
AND `persinst`.`persinst_id` = `persinst_translation`.`persinst_id`
AND `persinst`.`persinst_id` IN (" . implode(', ', $persinst_ids) . ")");
while ($cur = $result->fetch_row()) {
$name = (string)$cur[1];
if (!empty($cur[2]) || !empty($cur[3])) {
$name .= ' (' . $cur[2] . '-' . $cur[3] . ')';
}
$output[(int)$cur[0]] = $name;
}
$result->close();
if (!empty($persinst_ids_left = array_diff($persinst_ids, array_keys($output)))) {
$result = $mysqli_noda->do_read_query("SELECT `persinst_id`, `persinst_anzeigename`
FROM `persinst`
WHERE `persinst_id` IN (" . implode(', ', $persinst_ids_left) . ")");
while ($cur = $result->fetch_row()) {
$output[(int)$cur[0]] = (string)$cur[1];
}
$result->close();
}
if (count($output) > 1) {
asort($output);
}
return $output;
}
/**
* Static function for getting time names in bulk.
*
@ -335,4 +383,28 @@ final class NodaNameGetter {
return "";
}
/**
* Static function for getting the name for a group.
*
* @param MDMysqli $mysqli_noda DB connection.
* @param integer $group_id Group ID.
*
* @return string
*/
public static function getGroupName(MDMysqli $mysqli_noda, int $group_id):string {
$result = $mysqli_noda->query_by_stmt("SELECT `group_name`
FROM `group`
WHERE `group_id` = ?", "i", $group_id);
if ($cur = $result->fetch_row()) {
$result->close();
return MD_STD_IN::sanitize_text($cur[0]);
}
$result->close();
return "";
}
}

407
src/NodaSplitTime.php Normal file
View File

@ -0,0 +1,407 @@
<?PHP
/**
* Describes a time after splitting / transfer object.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Describes a time after splitting / transfer object.
*/
final class NodaSplitTime {
public const DEFAULT_DATE = '0001-01-01';
public readonly string $start_year;
public readonly string $end_year;
public readonly string $counting_time_month;
public readonly string $counting_time_day;
public readonly NodaCountingTimeIndicator $counting_time_indicator;
public readonly NodaTimeBeforeAfterIndicator $before_after_indicator;
public string $start_date;
public string $end_date;
/**
* Returns a single, exact date.
*
* @param string $year Year.
* @param string $month Month.
* @param string $day Day.
* @param NodaTimeBeforeAfterIndicator $before_after_indicator Determines if the time is exact or before / after.
*
* @return NodaSplitTime
*/
public static function genExactDate(string $year, string $month, string $day, NodaTimeBeforeAfterIndicator $before_after_indicator = NodaTimeBeforeAfterIndicator::none):NodaSplitTime {
$start_year = $end_year = $year;
$start_date = $end_date = $year . '-' . $month . '-' . $day;
if ($before_after_indicator === NodaTimeBeforeAfterIndicator::before
|| $before_after_indicator === NodaTimeBeforeAfterIndicator::until
) {
$start_year = $start_date = '?';
}
if ($before_after_indicator === NodaTimeBeforeAfterIndicator::after
|| $before_after_indicator === NodaTimeBeforeAfterIndicator::since
) {
$end_year = $end_date = '?';
}
return new NodaSplitTime($start_year, $end_year, $month, $day, start_date: $start_date, end_date: $end_date);
}
/**
* Validates the entered start year.
*
* @param string $input Input to validate.
*
* @return void
*/
private function _validateStartYear(string $input):void {
if (strlen($input) > 6) {
throw new MDgenericInvalidInputsException("Time statement longer than 6 characters is impossible");
}
if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::before
|| $this->before_after_indicator === NodaTimeBeforeAfterIndicator::until)
&& $input !== '?'
) {
throw new MDgenericInvalidInputsException("Times with no certain start need to have a question mark (?) entered as a start date");
}
}
/**
* Validates the entered end year.
*
* @param string $input Input to validate.
*
* @return void
*/
private function _validateEndYear(string $input):void {
if (strlen($input) > 6) {
throw new MDgenericInvalidInputsException("Time statement longer than 6 characters is impossible");
}
if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::after
|| $this->before_after_indicator === NodaTimeBeforeAfterIndicator::since)
&& $input !== '?'
) {
throw new MDgenericInvalidInputsException("Times with no certain end need to have a question mark (?) entered as a end date");
}
}
/**
* Pads to four digits. E.g. 2 > 02.
*
* @param string $input Input string.
*
* @return string
*/
public static function pad_to_two(string $input):string {
return \substr("00" . $input, -2);
}
/**
* Validates the entered month string.
*
* @param string $input Input to validate.
*
* @return string
*/
private function _validateCountingTimeMonth(string $input):string {
if (strlen($input) > 2) throw new MDgenericInvalidInputsException("Input too long");
if ($input === '00') {
return $input;
}
if (($parsedInt = filter_var(ltrim($input, "0"), FILTER_VALIDATE_INT)) === false) {
throw new MDgenericInvalidInputsException("Input value is not numeric");
}
if ($parsedInt > 12) {
throw new MDgenericInvalidInputsException("Attempted to set a month number beyond 12");
}
return self::pad_to_two($input);
}
/**
* Validates the entered day string.
*
* @param string $input Input to validate.
*
* @return string
*/
private function _validateCountingTimeDay(string $input):string {
if (strlen($input) > 2) throw new MDgenericInvalidInputsException("Input too long");
if ($input === '00') {
return $input;
}
if (($parsedInt = filter_var(ltrim($input, "0"), FILTER_VALIDATE_INT)) === false) {
throw new MDgenericInvalidInputsException("Input value is not numeric");
}
if ($parsedInt > 31) {
throw new MDgenericInvalidInputsException("Attempted to set a day number beyond 31");
}
return self::pad_to_two($input);
}
/**
* Returns a date time for the start of the time.
*
* @return DateTime
*/
public function startToDateTime():DateTime {
if ($this->counting_time_indicator === NodaCountingTimeIndicator::bce) {
return new DateTime('-' . $this->start_date);
}
else {
return new DateTime($this->start_date);
}
}
/**
* Returns a date time for the end of the time.
*
* @return DateTime
*/
public function endToDateTime():DateTime {
if ($this->counting_time_indicator === NodaCountingTimeIndicator::bce) {
return new DateTime('-' . $this->end_date);
}
else {
return new DateTime($this->end_date);
}
}
/**
* Generates a time name based on the current entry.
*
* @return string
*/
public function toTimeName():string {
$prefix = $this->before_after_indicator->toString();
if (!empty($prefix)) $prefix .= ' ';
// Determine start and end for display
if ($this->start_year === '?') {
$start = (int)$this->end_year;
}
else $start = (int)$this->start_year;
if ($this->end_year === '?') {
$end = (int)$this->start_year;
}
else $end = (int)$this->end_year;
if ($this->before_after_indicator === NodaTimeBeforeAfterIndicator::before && $this->counting_time_month === '00') {
$start++;
$end++;
}
else if ($this->before_after_indicator === NodaTimeBeforeAfterIndicator::after && $this->counting_time_month === '00') {
$start--;
$end--;
}
// Determine suffix
if ($start < 0 && $end < 0) {
$suffix = " v. Chr.";
}
else if ($end < 1000) {
$suffix = " n. Chr.";
}
else $suffix = "";
$start = \abs($start);
$end = \abs($end);
if ($start !== $end) {
return "{$prefix}{$start}-{$end}{$suffix}";
}
// A single day of a given month of a given (single) year
else if (\intval($this->counting_time_month) !== 0 and \intval($this->counting_time_day) !== 0) {
return "{$prefix}{$this->counting_time_day}.{$this->counting_time_month}.{$start}{$suffix}";
}
// A single year
else if ($start === $end && trim((string)$this->counting_time_month, " 0") === "" && trim((string)$this->counting_time_day, " 0") === "") {
return "{$prefix}{$start}{$suffix}";
}
// Single month of a given year
else if ($start === $end && trim((string)$this->counting_time_month, " 0") !== "" && trim((string)$this->counting_time_day, " 0") === "") {
$fmt = new IntlDateFormatter(
'de-DE',
IntlDateFormatter::FULL,
IntlDateFormatter::FULL,
null,
IntlDateFormatter::GREGORIAN,
'MMMM Y'
);
try {
return $prefix . $fmt->format(MD_STD::strtotime("{$start}-{$this->counting_time_month}-15 01:01:01")) . $suffix;
}
catch (MDInvalidInputDate $e) {
return "";
}
}
return "";
}
/**
* Returns an array in the old time splitter format (array with sex values).
*
* @return array<string>
*/
public function toOldFormat():array {
return [
$this->start_year,
$this->end_year,
$this->counting_time_month,
$this->counting_time_day,
$this->counting_time_indicator->toString(),
$this->before_after_indicator->toString(),
];
}
/**
* Constructor.
*
* @param string $start_year Year.
* @param string $end_year Year.
* @param string $counting_time_month Month.
* @param string $counting_time_day Day.
* @param NodaCountingTimeIndicator $counting_time_indicator Determines if the time is BCE or CCE.
* @param NodaTimeBeforeAfterIndicator $before_after_indicator Determines if the time is inexact to one direction.
* @param false|string $start_date Start date.
* @param false|string $end_date End date.
*
* @return void
*/
public function __construct(string $start_year, string $end_year,
string $counting_time_month = "00", string $counting_time_day = "00",
NodaCountingTimeIndicator $counting_time_indicator = NodaCountingTimeIndicator::ce,
NodaTimeBeforeAfterIndicator $before_after_indicator = NodaTimeBeforeAfterIndicator::none,
false|string $start_date = false,
false|string $end_date = false,
) {
if (substr($start_year, 0, 1) === '-') {
if (strlen($start_year) > 5) $start_year = '-' . str_pad(trim($start_year, '-'), 4, '-', STR_PAD_LEFT);
}
if ($start_date !== false && str_starts_with($start_date, '-')) {
$parts = explode('-', trim($start_date, '-'));
$parts[0] = str_pad($parts[0], 4, '0', STR_PAD_LEFT);
$start_date = '-' . implode('-', $parts);
}
if (substr($end_year, 0, 1) === '-') {
if (strlen($end_year) > 5) $end_year = '-' . str_pad(trim($end_year, '-'), 4, '-', STR_PAD_LEFT);
}
if ($end_date !== false && str_starts_with($end_date, '-')) {
$parts = explode('-', trim($end_date, '-'));
$parts[0] = str_pad($parts[0], 4, '0', STR_PAD_LEFT);
$end_date = '-' . implode('-', $parts);
}
$this->counting_time_indicator = $counting_time_indicator;
$this->before_after_indicator = $before_after_indicator;
$this->_validateStartYear($start_year);
$this->start_year = $start_year;
$this->_validateEndYear($end_year);
$this->end_year = $end_year;
$this->counting_time_month = $this->_validateCountingTimeMonth($counting_time_month);
$this->counting_time_day = $this->_validateCountingTimeDay($counting_time_day);
// Calculate start date and end date
if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::before
|| $this->before_after_indicator === NodaTimeBeforeAfterIndicator::until)
|| $start_date === '?'
) {
$this->start_date = '-9999-12-31';
}
if (($this->before_after_indicator === NodaTimeBeforeAfterIndicator::after
|| $this->before_after_indicator === NodaTimeBeforeAfterIndicator::since)
|| $end_date === '?'
) {
$this->end_date = '9999-12-31';
}
if (!isset($this->start_date) && false !== $start_date) {
$this->start_date = date("Y-m-d", MD_STD::strtotime($start_date));
}
if (!isset($this->end_date) && false !== $end_date) {
$this->end_date = date("Y-m-d", MD_STD::strtotime($end_date));
}
if (!isset($this->start_date)) {
if ($this->counting_time_month === "00") {
$this->start_date = $this->start_year . '-01-01';
}
else if ($this->counting_time_day === "00") {
$this->start_date = $this->start_year . '-' . $this->counting_time_month . '-01';
}
else {
throw new MDgenericInvalidInputsException("Cannot identify start date automatically");
}
}
if (!isset($this->end_date)) {
if ($this->counting_time_month === "00") {
$this->end_date = $this->end_year . '-12-31';
}
else if ($this->counting_time_day === "00") {
$this->end_date = $this->end_year . '-' . $this->counting_time_month . '-31';
}
else {
throw new MDgenericInvalidInputsException("Cannot identify end date automatically");
}
}
// Validate
$startDateTime = MD_STD::strtotime("2000-" . substr($this->start_date, -5));
if (checkdate((int)date('m', $startDateTime), (int)date('d', $startDateTime), (int)date('Y', $startDateTime)) === false) {
throw new MDgenericInvalidInputsException("Invalid start date: " . $this->start_date);
}
if (!empty((int)$this->counting_time_day)) {
// The year 2000 is used here as it is a leap year and lots of years accepted in md are not accepted
// by checkdate.
if (checkdate((int)$this->counting_time_month, (int)$this->counting_time_day, 2000) === false) {
throw new MDgenericInvalidInputsException("Invalid date formed by counting time: " . $this->counting_time_month . ' -- ' . $this->counting_time_day);
}
}
}
}

View File

@ -0,0 +1,64 @@
<?PHP
/**
* Identifies the type of tag relation to an object based on known suffixes.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Contains static functions for identifying uncertainty or blocking
* completely uncertain inputs for actors, times, and places.
*/
final class NodaTagRelationIdentifier {
private const SUFFIXES = [
'de' => [
' (Motiv)' => MDTagRelationType::display_subject,
' [Motiv]' => MDTagRelationType::display_subject,
' <Motiv>' => MDTagRelationType::display_subject,
' (Material)' => MDTagRelationType::material,
' [Material]' => MDTagRelationType::material,
' <Material>' => MDTagRelationType::material,
' (Technik)' => MDTagRelationType::technique,
' [Technik]' => MDTagRelationType::technique,
' <Technik>' => MDTagRelationType::technique,
]
];
public readonly string $name;
public readonly MDTagRelationType|false $relation;
/**
* Constructor: Removes identifiers for well-known tag relations and determines cleaned name and relation type.
*
* @param string $lang Current language.
* @param string $input_string Input string to clean.
*
* @return void
*/
public function __construct(string $lang, string $input_string) {
if (empty(self::SUFFIXES[$lang])) {
$this->name = $input_string;
$this->relation = false;
return;
}
$relation = false;
$suffixes = self::SUFFIXES[$lang];
foreach (array_keys($suffixes) as $suffix) {
if (\mb_substr($input_string, \mb_strlen($suffix) * -1) === "$suffix") {
$input_string = \mb_substr($input_string, 0, \mb_strlen($suffix) * -1);
$relation = $suffixes[$suffix];
}
}
$this->name = $input_string;
$this->relation = $relation;
}
}

View File

@ -13,7 +13,7 @@ final class NodaTimeAutotranslater {
// TODO: Move these to NodaTimeAutotranslaterLocales
const LANGS_SYLLABLE_CLEANING = [
public const LANGS_SYLLABLE_CLEANING = [
"hu" => [
"10-as évek" => "10-es évek",
"40-as évek" => "40-es évek",
@ -463,45 +463,52 @@ final class NodaTimeAutotranslater {
}
/**
* Gets translations for a given entry type.
* Prepares translations for each available language.
*
* @param array<integer|string> $timeInfo Time information.
*
* @return array<string>
*/
public static function getTranslations(array $timeInfo):array {
public static function prepareTranslations(array $timeInfo):array {
if (!empty($timeInfo['zeit_name']) and strlen((string)$timeInfo['zeit_name']) > 10 and !empty($timespanDates = NodaTimeSplitter::attempt_splitting_from_till((string)$timeInfo['zeit_name']))) {
$output = [];
$start = NodaTimeSplitter::attempt_splitting($timespanDates['start_name']);
if (empty($start = NodaTimeSplitter::attempt_splitting($timespanDates['start_name']))) {
return [];
}
$startTimeInfo = [
"zeit_name" => $timespanDates['start_name'],
"zeit_beginn" => $start[0],
"zeit_ende" => $start[1],
"zeit_beginn" => $start->start_year,
"zeit_ende" => $start->end_year,
"zeit_zaehlzeit_jahr" => NodaTimeSplitter::timePartsToCountingYear($start),
"zeit_zaehlzeit_monat" => $start[2],
"zeit_zaehlzeit_tag" => $start[3],
"zeit_zaehlzeit_vorzeichen" => $start[4],
"zeit_zaehlzeit_monat" => $start->counting_time_month,
"zeit_zaehlzeit_tag" => $start->counting_time_day,
"zeit_zaehlzeit_vorzeichen" => $start->counting_time_indicator->toString(),
];
$end = NodaTimeSplitter::attempt_splitting($timespanDates['end_name']);
if (empty($end = NodaTimeSplitter::attempt_splitting($timespanDates['end_name']))) {
return [];
}
$endTimeInfo = [
"zeit_name" => $timespanDates['end_name'],
"zeit_beginn" => $end[0],
"zeit_ende" => $end[1],
"zeit_beginn" => $end->start_year,
"zeit_ende" => $end->end_year,
"zeit_zaehlzeit_jahr" => NodaTimeSplitter::timePartsToCountingYear($end),
"zeit_zaehlzeit_monat" => $end[2],
"zeit_zaehlzeit_tag" => $end[3],
"zeit_zaehlzeit_vorzeichen" => $end[4],
"zeit_zaehlzeit_monat" => $end->counting_time_month,
"zeit_zaehlzeit_tag" => $end->counting_time_day,
"zeit_zaehlzeit_vorzeichen" => $end->counting_time_indicator->toString(),
];
$output = [];
$cases = NodaTimeAutotranslaterLocales::cases();
foreach ($cases as $tLang) {
$start_term = self::getTranslations($startTimeInfo)[$tLang->name];
$end_term = self::getTranslations($endTimeInfo)[$tLang->name];
$startTls = self::getTranslations($startTimeInfo);
$endTls = self::getTranslations($endTimeInfo);
if (empty($startTls) || empty($endTls)) return [];
$start_term = $startTls[$tLang->name];
$end_term = $endTls[$tLang->name];
$output[$tLang->name] = \sprintf($tLang->formatYearspanForSprintf(), $start_term, $end_term);
}
@ -600,6 +607,78 @@ final class NodaTimeAutotranslater {
}
/**
* Validates correctness of years in translation strings.
*
* @param string|integer $start Start year.
* @param string|integer $end End year.
* @param array<string, string> $translations Translations.
*
* @return boolean
*/
public static function validateTranslations(string|int $start, string|int $end, array $translations):bool {
$start = ltrim((string)$start, ' 0-');
$end = ltrim((string)$end, ' 0-');
// Edge cases: Centuries and decades have special translations
// and can thus not be validated properly
// Century BCE
if (substr($start, -1) === "0" && substr($end, -1) === '1' && $start > $end) {
return true;
}
// Century CE
if (substr($start, -1) === "1" && substr($end, -1) === '0' && $start < $end) {
return true;
}
// Decade
if (substr($start, -1) === "0" && substr($end, -1) === '9' && $start < $end) {
return true;
}
// 1920 + ? can be both Since 1920 and After 1919, so validation
// is impossible there, too
if ($start === '?' || $end === '?') return true;
// Unset unvalidatable languages
unset($translations['ar'], $translations['fa']);
if ($start !== '?') {
foreach ($translations as $t) {
if (!str_contains($t, $start)) {
return false;
}
}
}
if ($end !== '?' && $start !== $end) {
foreach ($translations as $t) {
if (!str_contains($t, $end)) {
return false;
}
}
}
return true;
}
/**
* Gets translations for a given entry type.
*
* @param array<integer|string> $timeInfo Time information.
*
* @return array<string>
*/
public static function getTranslations(array $timeInfo):array {
$output = self::prepareTranslations($timeInfo);
if (self::validateTranslations($timeInfo['zeit_beginn'], $timeInfo['zeit_ende'], $output) === false) return [];
return $output;
}
/**
* Runs autotranslater.
*
@ -609,7 +688,9 @@ final class NodaTimeAutotranslater {
*/
public function translate(array $timeInfo):void {
$translations = self::getTranslations($timeInfo);
if (empty($translations = self::getTranslations($timeInfo))) {
return;
}
$this->_mysqli_noda->autocommit(false);

View File

@ -0,0 +1,67 @@
<?PHP
/**
* Represents a time indicator (CE / BCE).
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Represents a time indicator (CE / BCE).
*/
enum NodaTimeBeforeAfterIndicator implements JsonSerializable {
case none;
case after;
case before;
case since;
case until;
/**
* Returns a value of this type based on a string.
*
* @param string $input Input to get a value from.
*
* @return NodaTimeBeforeAfterIndicator
*/
public static function fromString(string $input):NodaTimeBeforeAfterIndicator {
return match($input) {
'' => self::none,
'Nach' => self::after,
'Vor' => self::before,
'Seit' => self::since,
'Bis' => self::until,
default => throw new MDpageParameterNotFromListException("Unknown before / after indicator"),
};
}
/**
* Returns a canonical string representation.
*
* @return string
*/
public function toString():string {
return match($this) {
self::none => '',
self::after => 'Nach',
self::before => 'Vor',
self::since => 'Seit',
self::until => 'Bis',
};
}
/**
* Provides the option to serialize as a string during json_encode().
*
* @return string
*/
public function jsonSerialize():string {
return $this->name;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ declare(strict_types = 1);
*/
final class NodaUncertaintyHelper {
const PERSINST_INDICATORS_DISALLOWED = [
public const PERSINST_INDICATORS_DISALLOWED = [
"Unbekannt",
"unbekannt",
"Anonymus",
@ -31,30 +31,42 @@ final class NodaUncertaintyHelper {
"ismeretlen.",
"Ismeretlen.",
"ism.",
"невизначена", // "Uncertain" (ukr)
"невизначений", // "Unspecified" (ukr)
"не встановлено", // "Not established" (ukr)
"невизначено", // "Not established" (ukr)
"Невідом", // Unknown
"невизн", // Unknown
"Не визначен", // Not determined
"Невідомий артист", // Unknown artist
];
const PERSINST_UNCERTAINTY_PREFIXES = [
public const PERSINST_UNCERTAINTY_PREFIXES = [
"verm. ",
"Verm. ",
"vermtl. ",
"Vermtl. ",
"vermutl. ",
"Vermutl. ",
"vermutlich ",
"Vermutlich ",
"wahrscheinlich ",
"Wahrscheinlich ",
"wohl ",
"Wohl ",
"?",
];
const PERSINST_UNCERTAINTY_SUFFIXES = [
public const PERSINST_UNCERTAINTY_SUFFIXES = [
"(?)",
"?",
" [vermutlich]",
" vermutlich",
" [verm.]",
" [wahrscheinlich]",
];
const TIME_INDICATORS_DISALLOWED = [
public const TIME_INDICATORS_DISALLOWED = [
"Nachgewiesen",
"nachgewiesen",
"o.D.",
@ -76,12 +88,20 @@ final class NodaUncertaintyHelper {
"Neu",
"Neu hergestellt",
"zeitl. nicht faßbar",
"Без року", // Without a year
"Без дати", // Unknown date
"Не датовано", // Not dated
"Н.д.", // Not dated
"Без датування", // No dating
"б.р.", // No dating
"б.д.", // No dating
];
const TIME_UNCERTAINTY_PREFIXES = [
public const TIME_UNCERTAINTY_PREFIXES = [
"c. ",
"ca ",
"ca. ",
"ca.",
"Ca ",
"Ca. ",
"za. ",
@ -94,6 +114,8 @@ final class NodaUncertaintyHelper {
"Verm. ",
"vermtl. ",
"Vermtl. ",
"vermutl. ",
"Vermutl. ",
"vermutlich ",
"Vermutlich ",
"vermutlich um ",
@ -102,9 +124,17 @@ final class NodaUncertaintyHelper {
"wohl um ",
"Wohl ",
"Wohl um ",
"Приблизно", // UK: About / approximately
"близько", // UK: About
"около", // UK: About
"коло", // UK: About
"неточно", // UK: Inaccurate
"майже", // UK: Almost / nearly / about
"орієнтовно", // UK: approximately
"Прибл.", // UK: approximately
];
const TIME_UNCERTAINTY_SUFFIXES = [
public const TIME_UNCERTAINTY_SUFFIXES = [
"(?)",
"?",
" (ca.)",
@ -113,15 +143,19 @@ final class NodaUncertaintyHelper {
" [circa]",
" (verm.)",
" (vermutl.)",
" vermutlich",
" körül",
", um",
", ca.",
", ca",
" (um)",
" (ок.)",
];
/**
* Substrings used to express uncertainty about the validity of a place name.
*/
const PLACE_INDICATORS_DISALLOWED = [
public const PLACE_INDICATORS_DISALLOWED = [
"Unbekannt",
"unbekannt",
"Unknown",
@ -140,25 +174,50 @@ final class NodaUncertaintyHelper {
"o. O.",
"Diverse O. u. o.O.",
"o.O.",
"Без місця", // No place
"не вказано", // No place
"не вказане", // No place
"невідоме", // No place
];
const PLACE_UNCERTAINTY_PREFIXES = [
public const PLACE_UNCERTAINTY_PREFIXES = [
"ca ",
"Ca ",
"ca. ",
"Ca. ",
"circa ",
"Circa ",
"evtl ",
"Evtl ",
"evtl. ",
"Evtl. ",
"möglicherweise ",
"Möglicherweise ",
"vlt. ",
"wohl ",
"Wohl ",
"Vlt. ",
"verm. ",
"Verm. ",
"vermut. ",
"Vermut. ",
"vermtl. ",
"Vermtl. ",
"vermutl. ",
"Vermutl. ",
"vermutlich ",
"Vermutlich ",
"vermutlich: ",
"Vermutlich: ",
"wohl ",
"Wohl ",
"wahrsch. ",
"Wahrsch. ",
"wahrscheinlich ",
"Wahrscheinlich ",
"можливо",
"?",
];
const PLACE_UNCERTAINTY_SUFFIXES = [
public const PLACE_UNCERTAINTY_SUFFIXES = [
"(?)",
"(vermutl.)",
"[vermutl.]",
@ -178,8 +237,7 @@ final class NodaUncertaintyHelper {
*/
public static function trim(string $input):string {
$input = \trim($input, ", \t\n\r\n;-:");
return $input;
return \trim($input, ", \t\n\r\n;-:");
}
@ -226,7 +284,7 @@ final class NodaUncertaintyHelper {
*/
public static function guessTimeCertainty(string $zeit_name):bool {
$zeit_name = \strtolower($zeit_name);
$zeit_name = self::trim(strtolower($zeit_name));
// Attempt to guess uncertainty based on prefixes.
foreach (self::TIME_UNCERTAINTY_PREFIXES as $prefix) {
@ -275,6 +333,14 @@ final class NodaUncertaintyHelper {
}
}
// If brackets are included in the name, try removing prefixes and suffixes
// from the beginning.
if (($bracketPos = strpos($ort_name, "(")) !== false) {
$start = substr($ort_name, 0, $bracketPos);
$end = substr($ort_name, $bracketPos);
$ort_name = self::cleanUncertaintyIndicatorsPlace($start) . ' ' . $end;
}
return self::trim($ort_name);
}
@ -289,7 +355,7 @@ final class NodaUncertaintyHelper {
*/
public static function guessPlaceCertainty(string $ort_name):bool {
$ort_name = \strtolower($ort_name);
$ort_name = self::trim(\strtolower($ort_name));
// Attempt to guess uncertainty based on prefixes.
foreach (NodaUncertaintyHelper::PLACE_UNCERTAINTY_PREFIXES as $prefix) {
@ -305,6 +371,13 @@ final class NodaUncertaintyHelper {
}
}
// If brackets are included in the name, try the same for everything up to the
// first brackets.
if (($bracketPos = strpos($ort_name, "(")) !== false) {
$name = substr($ort_name, 0, $bracketPos);
return self::guessPlaceCertainty($name);
}
return true; // Certain / no uncertainty found
}
@ -350,7 +423,7 @@ final class NodaUncertaintyHelper {
*/
public static function guessPersinstCertainty(string $name):bool {
$name = \trim(\strtolower($name));
$name = self::trim(\strtolower($name));
// Attempt to guess uncertainty based on prefixes.
foreach (NodaUncertaintyHelper::PERSINST_UNCERTAINTY_PREFIXES as $prefix) {

View File

@ -11,7 +11,59 @@ declare(strict_types = 1);
*/
final class NodaValidationHelper {
const ACTOR_DESCRIPTION_REQUIRED_DISTINCT_CHARS = 2;
const ACTOR_DESCRIPTION_REQUIRED_DISTINCT_CHARS = 3;
/**
* Validates an actor description for completeness. Of course, only an informed
* guess based on the length and character composition of the description can be
* made.
*
* @param string $description Input descrition.
* @param string $name Names of the actor. Optional. Setting this enables
* checks e.g. to prevent duplicating the actor name
* as a description.
*
* @return void
*/
public static function validateTagDescription(string $description, string $name = ""):void {
// For nodac, empty tag descriptions are to be allowed. Thus, a
// dedicated check for fully empty ones with a dedicated exception
// is needed.
if (empty($description)) {
throw new MDInvalidEmptyInputException("No tag name is provided");
}
// Throw error on descriptions that are too short
if (\mb_strlen($description) < 10) {
throw new MDgenericInvalidInputsException("Tag description is too short");
}
// Validate tag description based on character composition.
// Ensure more than 3 distinct characters are used.
$chars = \str_split($description);
$uniqueChars = array_unique($chars);
if (count($uniqueChars) <= self::ACTOR_DESCRIPTION_REQUIRED_DISTINCT_CHARS) {
throw new MDgenericInvalidInputsException("There need to be more than " . self::ACTOR_DESCRIPTION_REQUIRED_DISTINCT_CHARS . " distinct characters.");
}
// Ensure more than the actor name is used.
$clearedChars = [' ' => ' ', ',' => ' ', ';' => ' ', '.' => ' '];
$uniqueNames = array_unique(array_diff(explode(' ', strtr($name, $clearedChars)), ['']));
sort($uniqueNames);
$descCleared = strtr($description, $clearedChars);
$descWords = array_unique(array_diff(explode(' ', $descCleared), ['']));
sort($descWords);
if ($uniqueNames === $descWords) {
throw new MDgenericInvalidInputsException("The tag name was simply repeated in the description. This is not enough.");
}
}
/**
* Validates an actor description for completeness. Of course, only an informed

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
<?PHP
/**
* This file contains an exception class to be thrown if a user attempts to load
* data from a Wikidata item specifically established for a disambiguation page.
*
* @file
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
/**
* Exception class to be thrown if a user attempts to load
* data from a Wikidata item specifically established for a disambiguation page.
*/
final class NodaWikidataFetcherDisambiguationIsDisallowedException extends MDgenericInvalidInputsException {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Attempted to load a disambiguation page. Please select the specific item you want to fetch to enrich the given entry: ' . $this->getMessage();
}
}

View File

@ -11,7 +11,7 @@ declare(strict_types = 1);
*/
final class NodaPersinstFulltextSyncManticore {
const FULL_SYNC_COMMIT_AFTER = 30000;
private const FULL_SYNC_COMMIT_AFTER = 30000;
/**
* Returns all names and descriptions in the different languages of a actor.
@ -188,6 +188,10 @@ final class NodaPersinstFulltextSyncManticore {
$mysqli_manticore->commit();
if (PHP_SAPI === 'cli' && $mysqli_noda->ping() === false) {
$mysqli_noda->reconnect();
}
// Sync translations
$result = $mysqli_noda->do_read_query("SELECT `persinst`.`persinst_id`, `trans_language`,

View File

@ -11,7 +11,7 @@ declare(strict_types = 1);
*/
final class NodaTagFulltextSyncManticore {
const FULL_SYNC_COMMIT_AFTER = 30000;
private const FULL_SYNC_COMMIT_AFTER = 30000;
/**
* Returns all names and descriptions in the different languages of a tag.
@ -139,6 +139,10 @@ final class NodaTagFulltextSyncManticore {
/**
* Synchronizes base entries.
*
* @param MDMysqli $mysqli_noda Connection to MySQL DB.
* @param MDMysqli $mysqli_manticore Connection to Manticore DB.
* @param string $databasename Name of the main noda database.
*
* @return void
*/
public static function runFullSyncForBaseEntries(MDMysqli $mysqli_noda, MDMysqli $mysqli_manticore, string $databasename):void {
@ -189,6 +193,10 @@ final class NodaTagFulltextSyncManticore {
/**
* Synchronizes translated entries.
*
* @param MDMysqli $mysqli_noda Connection to MySQL DB.
* @param MDMysqli $mysqli_manticore Connection to Manticore DB.
* @param string $databasename Name of the main noda database.
*
* @return void
*/
public static function runFullSyncForTranslatedEntries(MDMysqli $mysqli_noda, MDMysqli $mysqli_manticore, string $databasename):void {

View File

@ -8,6 +8,7 @@
enum NodaTimeAutotranslaterLocales {
case ar;
case crh;
case de;
case en;
case es;
@ -40,6 +41,7 @@ enum NodaTimeAutotranslaterLocales {
return match($lang) {
'ar' => static::ar,
'crh' => static::crh,
'de' => static::de,
'en' => static::en,
'es' => static::es,
@ -73,6 +75,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => 'ar_SY.utf8',
self::crh => 'uk_UA.utf8',
self::de => 'de_DE.utf8',
self::en => 'en_US.utf8',
self::es => 'es_ES.utf8',
@ -108,6 +111,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => 'ar-SY',
self::crh => 'uk-UA',
self::de => 'de-DE',
self::en => 'en-US',
self::es => 'es-ES',
@ -143,6 +147,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s',
self::crh => '%s',
self::de => '%s n. Chr.',
self::en => '%s CE',
self::es => '%s d.C.',
@ -176,6 +181,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '-%s',
self::crh => '%s рік до нашої ери',
self::de => '%s v. Chr.',
self::en => '%s BC',
self::es => '%s a.C.',
@ -211,6 +217,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s',
self::crh => '%s',
self::de => '%s',
self::en => '%s',
self::es => '%s',
@ -244,6 +251,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s-%s',
self::crh => '%s-%s',
self::de => '%s-%s',
self::en => '%s-%s',
self::es => '%s-%s',
@ -279,6 +287,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s-',
self::crh => 'з %s року',
self::de => 'Seit %s',
self::en => 'Since %s',
self::es => 'Desde %s',
@ -315,6 +324,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s-',
self::crh => 'після %s року',
self::de => 'Nach %s',
self::en => 'After %s',
self::es => 'Despues de %s',
@ -350,6 +360,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '-%s',
self::crh => 'до %s року',
self::de => 'Bis %s',
self::en => 'Until %s',
self::es => 'Hasta %s',
@ -384,6 +395,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => 'القرن ال %s',
self::crh => '%s століття',
self::de => '%s. Jahrhundert',
self::en => '%s. century',
self::es => 'Siglo %s',
@ -418,6 +430,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => 'القرن ال %s-%s',
self::crh => '%s-%s століття',
self::de => '%s.-%s. Jahrhundert',
self::en => '%s.-%s. century',
self::es => 'Siglo %s-%s',
@ -452,6 +465,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s-%s',
self::crh => '%s-ті роки',
self::de => '%ser Jahre',
self::en => '%ss',
self::es => '%s-%s',
@ -486,6 +500,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '%s-%s',
self::crh => '%s-%s-ті роки',
self::de => '%s-%ser Jahre',
self::en => '%s-%ss',
self::es => '%s-%s',
@ -521,6 +536,7 @@ enum NodaTimeAutotranslaterLocales {
return match($this) {
self::ar => '-%s',
self::crh => 'до %s року',
self::de => 'Vor %s',
self::en => 'Before %s',
self::es => 'Antes de %s',
@ -558,6 +574,7 @@ enum NodaTimeAutotranslaterLocales {
# self::be => '%d.%B.%Y',
# self::bg => '%Y-%B-%d',
# self::ca => '%d/%m/%Y',
self::crh => '%d.%m.%Y',
# self::cs => '%d.%B.%Y',
# self::da => '%d-%m-%Y',
self::de => '%d.%m.%Y',
@ -618,6 +635,7 @@ enum NodaTimeAutotranslaterLocales {
# self::be => '%d.%B.%Y',
# self::bg => '%Y-%B-%d',
# self::ca => '%d/%m/%Y',
self::crh => 'dd.MM.Y',
# self::cs => '%d.%B.%Y',
# self::da => '%d-%m-%Y',
self::de => 'dd.MM.Y',
@ -679,6 +697,7 @@ enum NodaTimeAutotranslaterLocales {
# self::bg => '%Y-%B',
# self::ca => '%m/%Y',
# self::cs => '%B.%Y',
self::crh => '%m %Y',
# self::da => '%m-%Y',
self::de => '%B %Y',
# self::el => '%B %Y',
@ -735,6 +754,7 @@ enum NodaTimeAutotranslaterLocales {
# self::bg => 'Y-MMMM',
# self::ca => 'MM/Y',
# self::cs => 'MMMM.Y',
self::crh => 'MMMM Y',
# self::da => 'MM-Y',
self::de => 'MMMM Y',
# self::el => 'MMMM Y',

View File

@ -0,0 +1 @@
["\u0634\u0645\u0627\u0644","\u062c\u0646\u0648\u0628","\u063a\u0631\u0628","\u0634\u0631\u0642","\u0441\u0435\u0432\u0435\u0440","\u042e\u0433","\u0417\u0430\u043f\u0430\u0434","\u0418\u0437\u0442\u043e\u043a","\u0989\u09a4\u09cd\u09a4\u09b0","\u09a6\u0995\u09cd\u09b7\u09bf\u09a3","\u09aa\u09b6\u09cd\u099a\u09bf\u09ae","\u09aa\u09c2\u09b0\u09cd\u09ac","sever","jih","z\u00e1pad","v\u00fdchod","nord","syd","vest","\u00f8st","Norden","S\u00fcden","Westen","Osten","\u03b2\u03bf\u03c1\u03c1\u03ac\u03c2","\u03bd\u03cc\u03c4\u03bf\u03c2","\u03b4\u03cd\u03c3\u03b7","\u03b1\u03bd\u03b1\u03c4\u03bf\u03bb\u03ae","north","south","west","east","norte","sur","oeste","este","\u0628\u0627\u062e\u062a\u0631","\u062e\u0627\u0648\u0631","pohjoinen","etel\u00e4","l\u00e4nsi","it\u00e4","sud","ouest","est","Arewa","Kudu","Yamma","Gabas","\u05e6\u05e4\u05d5\u05df","\u05d3\u05e8\u05d5\u05dd","\u05de\u05e2\u05e8\u05d1","\u05de\u05d6\u05e8\u05d7","\u0909\u0924\u094d\u0924\u0930","\u0926\u0915\u094d\u0937\u093f\u0923","\u092a\u0936\u094d\u091a\u093f\u092e","\u092a\u0942\u0930\u094d\u0935","\u00e9szak","d\u00e9l","nyugat","kelet","utara","selatan","barat","Timur","ovest","\u5317","\u5357","\u897f","\u6771","\u10e9\u10e0\u10d3\u10d8\u10da\u10dd\u10d4\u10d7\u10d8","\u10e1\u10d0\u10db\u10ee\u10e0\u10d4\u10d7\u10d8","\u10d3\u10d0\u10e1\u10d0\u10d5\u10da\u10d4\u10d7\u10d8","\u10d0\u10e6\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10d7\u10d8","\ubd81\ucabd","\ub0a8\ucabd","\uc11c\ucabd","\ub3d9\ucabd","noord","zuid","oost","p\u00f3\u0142noc","po\u0142udnie","zach\u00f3d","wsch\u00f3d","sul","leste","Sud","Vest","Est","\u044e\u0433","\u0437\u0430\u043f\u0430\u0434","\u0432\u043e\u0441\u0442\u043e\u043a","norr","s\u00f6der","v\u00e4ster","\u00f6ster","kaskazini","Kusini","Magharibi","Mashariki","\u0bb5\u0b9f\u0b95\u0bcd\u0b95\u0bc1","\u0ba4\u0bc6\u0bb1\u0bcd\u0b95\u0bc1","\u0bae\u0bc7\u0bb1\u0bcd\u0b95\u0bc1","\u0b95\u0bbf\u0bb4\u0b95\u0bcd\u0b95\u0bc1","\u0e17\u0e34\u0e28\u0e40\u0e2b\u0e19\u0e37\u0e2d","\u0e17\u0e34\u0e28\u0e43\u0e15\u0e49","\u0e17\u0e34\u0e28\u0e15\u0e30\u0e27\u0e31\u0e19\u0e15\u0e01","\u0e17\u0e34\u0e28\u0e15\u0e30\u0e27\u0e31\u0e19\u0e2d\u0e2d\u0e01","Hilaga","Timog","kanluran","kuzey","g\u00fcney","bat\u0131","do\u011fu","\u043f\u0456\u0432\u043d\u0456\u0447","\u043f\u0456\u0432\u0434\u0435\u043d\u044c","\u0437\u0430\u0445\u0456\u0434","\u0441\u0445\u0456\u0434","\u0645\u063a\u0631\u0628-\u0633\u0645\u062a","\u0645\u0634\u0631\u0642","h\u01b0\u1edbng b\u1eafc","h\u01b0\u1edbng nam","h\u01b0\u1edbng t\u00e2y","h\u01b0\u1edbng \u0111\u00f4ng","Nord","Ost","West","S\u00fcd"]

1
static/countries.ar.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.bg.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.bn.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.cs.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.da.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.de.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.el.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.en.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.es.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.fa.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.fi.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.fr.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ha.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.he.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.hi.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.hu.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.id.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.it.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ja.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ka.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ko.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.nl.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.pl.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.pt.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ro.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ru.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.sv.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.sw.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ta.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.th.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.tl.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.tr.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.uk.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.ur.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.vi.json Normal file

File diff suppressed because one or more lines are too long

1
static/countries.zh.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More