xref: /webtrees/app/Module/FixNameTags.php (revision 7413816e6dd2d50e569034fb804f3dce7471bb94)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Module;
21
22use Fisharebest\Webtrees\Elements\NameType;
23use Fisharebest\Webtrees\Fact;
24use Fisharebest\Webtrees\GedcomRecord;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Services\DataFixService;
27use Fisharebest\Webtrees\Tree;
28use Illuminate\Database\Query\Builder;
29use Illuminate\Support\Collection;
30
31use function array_keys;
32use function implode;
33use function preg_match;
34use function str_replace;
35
36/**
37 * Class FixNameTags
38 */
39class FixNameTags extends AbstractModule implements ModuleDataFixInterface
40{
41    use ModuleDataFixTrait;
42
43    // https://legacyfamilytree.se/WEB_US/user_defined_gedcom_tags.htm
44    private const CONVERT = [
45        '_ADPN'  => NameType::VALUE_ADOPTED,
46        '_AKA'   => NameType::VALUE_AKA,
47        '_AKAN'  => NameType::VALUE_AKA,
48        '_BIRN'  => NameType::VALUE_BIRTH,
49        '_CENN'  => '', // Census name
50        '_CURN'  => '', // Currently known as
51        '_FARN'  => NameType::VALUE_ESTATE,
52        '_FKAN'  => NameType::VALUE_AKA, // Formerly known as
53        '_GERN'  => '', // German name
54        '_HEB'   => '', // Hebrew name
55        '_HEBN'  => '', // Hebrew name
56        '_INDN'  => '', // Indian name
57        '_MARNM' => NameType::VALUE_MARRIED,
58        '_OTHN'  => NameType::VALUE_AKA, // Other name
59        '_RELN'  => NameType::VALUE_RELIGIOUS,
60        '_SHON'  => NameType::VALUE_AKA, // Short name
61        '_SLDN'  => NameType::VALUE_AKA, // Soldier name
62    ];
63
64    private DataFixService $data_fix_service;
65
66    /**
67     * @param DataFixService $data_fix_service
68     */
69    public function __construct(DataFixService $data_fix_service)
70    {
71        $this->data_fix_service = $data_fix_service;
72    }
73
74    /**
75     * How should this module be identified in the control panel, etc.?
76     *
77     * @return string
78     */
79    public function title(): string
80    {
81        /* I18N: Name of a module */
82        return I18N::translate('Convert %s tags to GEDCOM 5.5.1', 'INDI:NAME:_XXX');
83    }
84
85    public function description(): string
86    {
87        /* I18N: Description of a “Data fix” module */
88        return I18N::translate('Some genealogy software stores all names in a single name record, using custom tags such as _MARNM and _AKA. An alternative is to create a new name record for each name.');
89    }
90
91    /**
92     * XREFs of media records that might need fixing.
93     *
94     * @param Tree                 $tree
95     * @param array<string,string> $params
96     *
97     * @return Collection<int,string>
98     */
99    public function individualsToFix(Tree $tree, array $params): Collection
100    {
101        return $this->individualsToFixQuery($tree, $params)
102            ->where(static function (Builder $query): void {
103                foreach (array_keys(self::CONVERT) as $tag) {
104                    $query->orWhere('i_gedcom', 'LIKE', "%\n2 " . $tag . ' %');
105                }
106            })
107            ->pluck('i_id');
108    }
109
110    /**
111     * Does a record need updating?
112     *
113     * @param GedcomRecord         $record
114     * @param array<string,string> $params
115     *
116     * @return bool
117     */
118    public function doesRecordNeedUpdate(GedcomRecord $record, array $params): bool
119    {
120        $tags = implode('|', array_keys(self::CONVERT));
121
122        return preg_match('/\n1 NAME.*(?:\n[2-9] .*)*\n2 (' . $tags . ')/', $record->gedcom()) === 1;
123    }
124
125    /**
126     * Show the changes we would make
127     *
128     * @param GedcomRecord         $record
129     * @param array<string,string> $params
130     *
131     * @return string
132     */
133    public function previewUpdate(GedcomRecord $record, array $params): string
134    {
135        $diffs = [];
136
137        foreach ($record->facts(['NAME'], false, null, true) as $name) {
138            $old = $name->gedcom();
139            $new = $this->updateGedcom($name);
140
141            if ($old !== $new) {
142                $diffs[] = $this->data_fix_service->gedcomDiff($record->tree(), $old, $new);
143            }
144        }
145
146        return implode('<hr>', $diffs);
147    }
148
149    /**
150     * Fix a record
151     *
152     * @param GedcomRecord         $record
153     * @param array<string,string> $params
154     *
155     * @return void
156     */
157    public function updateRecord(GedcomRecord $record, array $params): void
158    {
159        $names = $record->facts(['NAME'], false, null, true);
160
161        foreach ($names as $name) {
162            $old = $name->gedcom();
163            $new = $this->updateGedcom($name);
164
165            if ($old !== $new) {
166                $record->updateFact($name->id(), $new, false);
167            }
168        }
169    }
170
171    /**
172     * @param Fact $fact
173     *
174     * @return string
175     */
176    private function updateGedcom(Fact $fact): string
177    {
178        $gedcom    = $fact->gedcom();
179        $converted = '';
180
181        $tags = implode('|', array_keys(self::CONVERT));
182
183        while (preg_match('/\n2 (' . $tags . ') (.+)((?:\n[3-9].*)*)/', $gedcom, $match)) {
184            $type = self::CONVERT[$match[1]];
185            if ($type !== '') {
186                $type = "\n2 TYPE " . $type;
187            }
188            $gedcom = str_replace($match[0], '', $gedcom);
189
190            $subtags = strtr($match[3], [
191                "\n3" => "\n2",
192                "\n4" => "\n3",
193                "\n5" => "\n4",
194                "\n6" => "\n5",
195            ]);
196            $converted .= "\n1 NAME " . $match[2] . $type . $subtags;
197        }
198
199        return $gedcom . $converted;
200    }
201}
202