. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Statistics\Repository; use Fisharebest\Webtrees\Database; use Fisharebest\Webtrees\Family; use Fisharebest\Webtrees\Functions\FunctionsDate; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Statistics\Google\ChartChildren; use Fisharebest\Webtrees\Statistics\Google\ChartDivorce; use Fisharebest\Webtrees\Statistics\Google\ChartFamily; use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest; use Fisharebest\Webtrees\Statistics\Google\ChartMarriage; use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge; use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies; use Fisharebest\Webtrees\Statistics\Helper\Sql; use Fisharebest\Webtrees\Tree; use stdClass; /** * */ class FamilyRepository { /** * @var Tree */ private $tree; /** * Constructor. * * @param Tree $tree */ public function __construct(Tree $tree) { $this->tree = $tree; } /** * General query on family. * * @param string $type * * @return string */ private function familyQuery(string $type): string { $rows = $this->runSql( " SELECT f_numchil AS tot, f_id AS id" . " FROM `##families`" . " WHERE" . " f_file={$this->tree->id()}" . " AND f_numchil = (" . " SELECT max( f_numchil )" . " FROM `##families`" . " WHERE f_file ={$this->tree->id()}" . " )" . " LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; $family = Family::getInstance($row->id, $this->tree); if (!$family) { return ''; } switch ($type) { default: case 'full': if ($family->canShow()) { $result = $family->formatList(); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'size': $result = I18N::number((int) $row->tot); break; case 'name': $result = '' . $family->getFullName() . ''; break; } return $result; } /** * Run an SQL query and cache the result. * * @param string $sql * * @return stdClass[] */ private function runSql($sql): array { return Sql::runSql($sql); } /** * Find the family with the most children. * * @return string */ public function largestFamily(): string { return $this->familyQuery('full'); } /** * Find the number of children in the largest family. * * @return string */ public function largestFamilySize(): string { return $this->familyQuery('size'); } /** * Find the family with the most children. * * @return string */ public function largestFamilyName(): string { return $this->familyQuery('name'); } /** * Find the couple with the most grandchildren. * * @param int $total * * @return array */ private function topTenGrandFamilyQuery(int $total): array { $rows = $this->runSql( "SELECT COUNT(*) AS tot, f_id AS id" . " FROM `##families`" . " JOIN `##link` AS children ON children.l_file = {$this->tree->id()}" . " JOIN `##link` AS mchildren ON mchildren.l_file = {$this->tree->id()}" . " JOIN `##link` AS gchildren ON gchildren.l_file = {$this->tree->id()}" . " WHERE" . " f_file={$this->tree->id()} AND" . " children.l_from=f_id AND" . " children.l_type='CHIL' AND" . " children.l_to=mchildren.l_from AND" . " mchildren.l_type='FAMS' AND" . " mchildren.l_to=gchildren.l_from AND" . " gchildren.l_type='CHIL'" . " GROUP BY id" . " ORDER BY tot DESC" . " LIMIT " . $total ); if (!isset($rows[0])) { return []; } $top10 = []; foreach ($rows as $row) { $family = Family::getInstance($row->id, $this->tree); if ($family && $family->canShow()) { $total = (int) $row->tot; $top10[] = [ 'family' => $family, 'count' => $total, ]; } } // TODO // if (I18N::direction() === 'rtl') { // $top10 = str_replace([ // '[', // ']', // '(', // ')', // '+', // ], [ // '‏[', // '‏]', // '‏(', // '‏)', // '‏+', // ], $top10); // } return $top10; } /** * Find the couple with the most grandchildren. * * @param int $total * * @return string */ public function topTenLargestGrandFamily(int $total = 10): string { $records = $this->topTenGrandFamilyQuery($total); return view( 'statistics/families/top10-nolist-grand', [ 'records' => $records, ] ); } /** * Find the couple with the most grandchildren. * * @param int $total * * @return string */ public function topTenLargestGrandFamilyList(int $total = 10): string { $records = $this->topTenGrandFamilyQuery($total); return view( 'statistics/families/top10-list-grand', [ 'records' => $records, ] ); } /** * Find the families with no children. * * @return int */ private function noChildrenFamiliesQuery(): int { $rows = $this->runSql( " SELECT COUNT(*) AS tot" . " FROM `##families`" . " WHERE f_numchil = 0 AND f_file = {$this->tree->id()}" ); return (int) $rows[0]->tot; } /** * Find the families with no children. * * @return string */ public function noChildrenFamilies(): string { return I18N::number($this->noChildrenFamiliesQuery()); } /** * Find the families with no children. * * @param string $type * * @return string */ public function noChildrenFamiliesList($type = 'list'): string { $rows = $this->runSql( " SELECT f_id AS family" . " FROM `##families` AS fam" . " WHERE f_numchil = 0 AND fam.f_file = {$this->tree->id()}" ); if (!isset($rows[0])) { return ''; } $top10 = []; foreach ($rows as $row) { $family = Family::getInstance($row->family, $this->tree); if ($family->canShow()) { if ($type === 'list') { $top10[] = '
  • ' . $family->getFullName() . '
  • '; } else { $top10[] = '' . $family->getFullName() . ''; } } } if ($type === 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace([ '[', ']', '(', ')', '+', ], [ '‏[', '‏]', '‏(', '‏)', '‏+', ], $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Create a chart of children with no families. * * @param string $size * @param int $year1 * @param int $year2 * * @return string */ public function chartNoChildrenFamilies(string $size = '220x200', int $year1 = -1, int $year2 = -1): string { $no_child_fam = $this->noChildrenFamiliesQuery(); return (new ChartNoChildrenFamilies($this->tree)) ->chartNoChildrenFamilies($no_child_fam, $size, $year1, $year2); } /** * Returns the ages between siblings. * * @param int $total The total number of records to query * * @return array */ private function ageBetweenSiblingsQuery(int $total): array { $rows = $this->runSql( " SELECT DISTINCT" . " link1.l_from AS family," . " link1.l_to AS ch1," . " link2.l_to AS ch2," . " child1.d_julianday2-child2.d_julianday2 AS age" . " FROM `##link` AS link1" . " LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->id()}" . " LEFT JOIN `##dates` AS child2 ON child2.d_file = {$this->tree->id()}" . " LEFT JOIN `##link` AS link2 ON link2.l_file = {$this->tree->id()}" . " WHERE" . " link1.l_file = {$this->tree->id()} AND" . " link1.l_from = link2.l_from AND" . " link1.l_type = 'CHIL' AND" . " child1.d_gid = link1.l_to AND" . " child1.d_fact = 'BIRT' AND" . " link2.l_type = 'CHIL' AND" . " child2.d_gid = link2.l_to AND" . " child2.d_fact = 'BIRT' AND" . " child1.d_julianday2 > child2.d_julianday2 AND" . " child2.d_julianday2 <> 0 AND" . " child1.d_gid <> child2.d_gid" . " ORDER BY age DESC" . " LIMIT " . $total ); if (!isset($rows[0])) { return []; } return $rows; } /** * Returns the calculated age the time of event. * * @param int $age The age from the database record * * @return string */ private function calculateAge(int $age): string { if ((int) ($age / 365.25) > 0) { $result = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $result = (int) ($age / 30.4375) . 'm'; } else { $result = $age . 'd'; } return FunctionsDate::getAgeAtEvent($result); } /** * Find the ages between siblings. * * @param int $total The total number of records to query * * @return array * @throws \Exception */ private function ageBetweenSiblingsNoList(int $total): array { $rows = $this->ageBetweenSiblingsQuery($total); foreach ($rows as $fam) { $family = Family::getInstance($fam->family, $this->tree); $child1 = Individual::getInstance($fam->ch1, $this->tree); $child2 = Individual::getInstance($fam->ch2, $this->tree); if ($child1->canShow() && $child2->canShow()) { // ! Single array (no list) return [ 'child1' => $child1, 'child2' => $child2, 'family' => $family, 'age' => $this->calculateAge((int) $fam->age), ]; } } return []; } /** * Find the ages between siblings. * * @param int $total The total number of records to query * @param bool $one Include each family only once if true * * @return array * @throws \Exception */ private function ageBetweenSiblingsList(int $total, bool $one): array { $rows = $this->ageBetweenSiblingsQuery($total); $top10 = []; $dist = []; foreach ($rows as $fam) { $family = Family::getInstance($fam->family, $this->tree); $child1 = Individual::getInstance($fam->ch1, $this->tree); $child2 = Individual::getInstance($fam->ch2, $this->tree); $age = $this->calculateAge((int) $fam->age); if ($one && !\in_array($fam->family, $dist, true)) { if ($child1->canShow() && $child2->canShow()) { $top10[] = [ 'child1' => $child1, 'child2' => $child2, 'family' => $family, 'age' => $age, ]; $dist[] = $fam->family; } } elseif (!$one && $child1->canShow() && $child2->canShow()) { $top10[] = [ 'child1' => $child1, 'child2' => $child2, 'family' => $family, 'age' => $age, ]; } } // TODO // if (I18N::direction() === 'rtl') { // $top10 = str_replace([ // '[', // ']', // '(', // ')', // '+', // ], [ // '‏[', // '‏]', // '‏(', // '‏)', // '‏+', // ], $top10); // } return $top10; } /** * Find the ages between siblings. * * @param int $total The total number of records to query * * @return string */ private function ageBetweenSiblingsAge(int $total): string { $rows = $this->ageBetweenSiblingsQuery($total); foreach ($rows as $fam) { return $this->calculateAge((int) $fam->age); } return ''; } /** * Find the ages between siblings. * * @param int $total The total number of records to query * * @return string * @throws \Exception */ private function ageBetweenSiblingsName(int $total): string { $rows = $this->ageBetweenSiblingsQuery($total); foreach ($rows as $fam) { $family = Family::getInstance($fam->family, $this->tree); $child1 = Individual::getInstance($fam->ch1, $this->tree); $child2 = Individual::getInstance($fam->ch2, $this->tree); if ($child1->canShow() && $child2->canShow()) { $return = '' . $child2->getFullName() . ' '; $return .= I18N::translate('and') . ' '; $return .= '' . $child1->getFullName() . ''; $return .= ' [' . I18N::translate('View this family') . ']'; } else { $return = I18N::translate('This information is private and cannot be shown.'); } return $return; } return ''; } /** * Find the names of siblings with the widest age gap. * * @param int $total * * @return string */ public function topAgeBetweenSiblingsName(int $total = 10): string { return $this->ageBetweenSiblingsName($total); } /** * Find the widest age gap between siblings. * * @param int $total * * @return string */ public function topAgeBetweenSiblings(int $total = 10): string { return $this->ageBetweenSiblingsAge($total); } /** * Find the name of siblings with the widest age gap. * * @param int $total * * @return string */ public function topAgeBetweenSiblingsFullName(int $total = 10): string { $record = $this->ageBetweenSiblingsNoList($total); return view( 'statistics/families/top10-nolist-age', [ 'record' => $record, ] ); } /** * Find the siblings with the widest age gaps. * * @param int $total * @param string $one * * @return string */ public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string { $records = $this->ageBetweenSiblingsList($total, (bool) $one); return view( 'statistics/families/top10-list-age', [ 'records' => $records, ] ); } /** * General query on familes/children. * * @param string $sex * @param int $year1 * @param int $year2 * * @return stdClass[] */ public function statsChildrenQuery(string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array { if ($sex === 'M') { $sql = "SELECT num, COUNT(*) AS total FROM " . "(SELECT count(i_sex) AS num FROM `##link` " . "LEFT OUTER JOIN `##individuals` " . "ON l_from=i_id AND l_file=i_file AND i_sex='M' AND l_type='FAMC' " . "JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->id()} GROUP BY l_to" . ") boys" . " GROUP BY num" . " ORDER BY num"; } elseif ($sex === 'F') { $sql = "SELECT num, COUNT(*) AS total FROM " . "(SELECT count(i_sex) AS num FROM `##link` " . "LEFT OUTER JOIN `##individuals` " . "ON l_from=i_id AND l_file=i_file AND i_sex='F' AND l_type='FAMC' " . "JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->id()} GROUP BY l_to" . ") girls" . " GROUP BY num" . " ORDER BY num"; } else { $sql = "SELECT f_numchil, COUNT(*) AS total FROM `##families` "; if ($year1 >= 0 && $year2 >= 0) { $sql .= "AS fam LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . " WHERE" . " married.d_gid = fam.f_id AND" . " fam.f_file = {$this->tree->id()} AND" . " married.d_fact = 'MARR' AND" . " married.d_year BETWEEN '{$year1}' AND '{$year2}'"; } else { $sql .= "WHERE f_file={$this->tree->id()}"; } $sql .= ' GROUP BY f_numchil'; } return $this->runSql($sql); } /** * Genearl query on families/children. * * @param string $size * * @return string */ public function statsChildren(string $size = '220x200'): string { return (new ChartChildren($this->tree)) ->chartChildren($size); } /** * Count the total children. * * @return string */ public function totalChildren(): string { $total = (int) Database::prepare( "SELECT SUM(f_numchil) FROM `##families` WHERE f_file = :tree_id" )->execute([ 'tree_id' => $this->tree->id(), ])->fetchOne(); return I18N::number($total); } /** * Find the average number of children in families. * * @return string */ public function averageChildren(): string { $average = (float) Database::prepare( "SELECT AVG(f_numchil) AS tot FROM `##families` WHERE f_file = :tree_id" )->execute([ 'tree_id' => $this->tree->id(), ])->fetchOne(); return I18N::number($average, 2); } /** * General query on families. * * @param int $total * * @return array */ private function topTenFamilyQuery(int $total): array { $rows = $this->runSql( "SELECT f_numchil AS tot, f_id AS id" . " FROM `##families`" . " WHERE" . " f_file={$this->tree->id()}" . " ORDER BY tot DESC" . " LIMIT " . $total ); if (empty($rows)) { return []; } $top10 = []; foreach ($rows as $row) { $family = Family::getInstance($row->id, $this->tree); if ($family && $family->canShow()) { $top10[] = [ 'family' => $family, 'count' => (int) $row->tot, ]; } } // TODO // if (I18N::direction() === 'rtl') { // $top10 = str_replace([ // '[', // ']', // '(', // ')', // '+', // ], [ // '‏[', // '‏]', // '‏(', // '‏)', // '‏+', // ], $top10); // } return $top10; } /** * The the families with the most children. * * @param int $total * * @return string */ public function topTenLargestFamily(int $total = 10): string { $records = $this->topTenFamilyQuery($total); return view( 'statistics/families/top10-nolist', [ 'records' => $records, ] ); } /** * Find the families with the most children. * * @param int $total * * @return string */ public function topTenLargestFamilyList(int $total = 10): string { $records = $this->topTenFamilyQuery($total); return view( 'statistics/families/top10-list', [ 'records' => $records, ] ); } /** * Create a chart of the largest families. * * @param string|null $size * @param string|null $color_from * @param string|null $color_to * @param int $total * * @return string */ public function chartLargestFamilies( string $size = null, string $color_from = null, string $color_to = null, int $total = 10 ): string { return (new ChartFamilyLargest($this->tree)) ->chartLargestFamilies($size, $color_from, $color_to, $total); } /** * Find the month in the year of the birth of the first child. * * @param bool $sex * * @return stdClass[] */ public function monthFirstChildQuery(bool $sex = false): array { if ($sex) { $sql_sex1 = ', i_sex'; $sql_sex2 = " JOIN `##individuals` AS child ON child1.d_file = i_file AND child1.d_gid = child.i_id "; } else { $sql_sex1 = ''; $sql_sex2 = ''; } $sql = "SELECT d_month{$sql_sex1}, COUNT(*) AS total " . "FROM (" . " SELECT family{$sql_sex1}, MIN(date) AS d_date, d_month" . " FROM (" . " SELECT" . " link1.l_from AS family," . " link1.l_to AS child," . " child1.d_julianday2 AS date," . " child1.d_month as d_month" . $sql_sex1 . " FROM `##link` AS link1" . " LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->id()}" . $sql_sex2 . " WHERE" . " link1.l_file = {$this->tree->id()} AND" . " link1.l_type = 'CHIL' AND" . " child1.d_gid = link1.l_to AND" . " child1.d_fact = 'BIRT' AND" . " child1.d_month IN ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')" . " ORDER BY date" . " ) AS children" . " GROUP BY family, d_month{$sql_sex1}" . ") AS first_child " . "GROUP BY d_month"; if ($sex) { $sql .= ', i_sex'; } return $this->runSql($sql); } /** * Number of husbands. * * @return string */ public function totalMarriedMales(): string { $n = (int) Database::prepare( "SELECT COUNT(DISTINCT f_husb) FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\\n1 MARR%'" )->execute([ 'tree_id' => $this->tree->id(), ])->fetchOne(); return I18N::number($n); } /** * Number of wives. * * @return string */ public function totalMarriedFemales(): string { $n = (int) Database::prepare( "SELECT COUNT(DISTINCT f_wife) FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\\n1 MARR%'" )->execute([ 'tree_id' => $this->tree->id(), ])->fetchOne(); return I18N::number($n); } /** * General query on parents. * * @param string $type * @param string $age_dir * @param string $sex * @param bool $show_years * * @return string */ private function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string { if ($sex === 'F') { $sex_field = 'WIFE'; } else { $sex_field = 'HUSB'; } if ($age_dir !== 'ASC') { $age_dir = 'DESC'; } $rows = $this->runSql( " SELECT" . " parentfamily.l_to AS id," . " childbirth.d_julianday2-birth.d_julianday1 AS age" . " FROM `##link` AS parentfamily" . " JOIN `##link` AS childfamily ON childfamily.l_file = {$this->tree->id()}" . " JOIN `##dates` AS birth ON birth.d_file = {$this->tree->id()}" . " JOIN `##dates` AS childbirth ON childbirth.d_file = {$this->tree->id()}" . " WHERE" . " birth.d_gid = parentfamily.l_to AND" . " childfamily.l_to = childbirth.d_gid AND" . " childfamily.l_type = 'CHIL' AND" . " parentfamily.l_type = '{$sex_field}' AND" . " childfamily.l_from = parentfamily.l_from AND" . " parentfamily.l_file = {$this->tree->id()} AND" . " birth.d_fact = 'BIRT' AND" . " childbirth.d_fact = 'BIRT' AND" . " birth.d_julianday1 <> 0 AND" . " childbirth.d_julianday2 > birth.d_julianday1" . " ORDER BY age {$age_dir} LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; if (isset($row->id)) { $person = Individual::getInstance($row->id, $this->tree); } switch ($type) { default: case 'full': if ($person && $person->canShow()) { $result = $person->formatList(); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'name': $result = '' . $person->getFullName() . ''; break; case 'age': $age = $row->age; if ($show_years) { $result = $this->calculateAge((int) $row->age); } else { $result = (string) floor($age / 365.25); } break; } return $result; } /** * Find the youngest mother * * @return string */ public function youngestMother(): string { return $this->parentsQuery('full', 'ASC', 'F', false); } /** * Find the name of the youngest mother. * * @return string */ public function youngestMotherName(): string { return $this->parentsQuery('name', 'ASC', 'F', false); } /** * Find the age of the youngest mother. * * @param string $show_years * * @return string */ public function youngestMotherAge(string $show_years = ''): string { return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years); } /** * Find the oldest mother. * * @return string */ public function oldestMother(): string { return $this->parentsQuery('full', 'DESC', 'F', false); } /** * Find the name of the oldest mother. * * @return string */ public function oldestMotherName(): string { return $this->parentsQuery('name', 'DESC', 'F', false); } /** * Find the age of the oldest mother. * * @param string $show_years * * @return string */ public function oldestMotherAge(string $show_years = ''): string { return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years); } /** * Find the youngest father. * * @return string */ public function youngestFather(): string { return $this->parentsQuery('full', 'ASC', 'M', false); } /** * Find the name of the youngest father. * * @return string */ public function youngestFatherName(): string { return $this->parentsQuery('name', 'ASC', 'M', false); } /** * Find the age of the youngest father. * * @param string $show_years * * @return string */ public function youngestFatherAge(string $show_years = ''): string { return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years); } /** * Find the oldest father. * * @return string */ public function oldestFather(): string { return $this->parentsQuery('full', 'DESC', 'M', false); } /** * Find the name of the oldest father. * * @return string */ public function oldestFatherName(): string { return $this->parentsQuery('name', 'DESC', 'M', false); } /** * Find the age of the oldest father. * * @param string $show_years * * @return string */ public function oldestFatherAge(string $show_years = ''): string { return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years); } /** * General query on age at marriage. * * @param string $type * @param string $age_dir * @param int $total * * @return string */ private function ageOfMarriageQuery(string $type, string $age_dir, int $total): string { if ($age_dir !== 'ASC') { $age_dir = 'DESC'; } $hrows = $this->runSql( " SELECT DISTINCT fam.f_id AS family, MIN(husbdeath.d_julianday2-married.d_julianday1) AS age" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . " LEFT JOIN `##dates` AS husbdeath ON husbdeath.d_file = {$this->tree->id()}" . " WHERE" . " fam.f_file = {$this->tree->id()} AND" . " husbdeath.d_gid = fam.f_husb AND" . " husbdeath.d_fact = 'DEAT' AND" . " married.d_gid = fam.f_id AND" . " married.d_fact = 'MARR' AND" . " married.d_julianday1 < husbdeath.d_julianday2 AND" . " married.d_julianday1 <> 0" . " GROUP BY family" . " ORDER BY age {$age_dir}" ); $wrows = $this->runSql( " SELECT DISTINCT fam.f_id AS family, MIN(wifedeath.d_julianday2-married.d_julianday1) AS age" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . " LEFT JOIN `##dates` AS wifedeath ON wifedeath.d_file = {$this->tree->id()}" . " WHERE" . " fam.f_file = {$this->tree->id()} AND" . " wifedeath.d_gid = fam.f_wife AND" . " wifedeath.d_fact = 'DEAT' AND" . " married.d_gid = fam.f_id AND" . " married.d_fact = 'MARR' AND" . " married.d_julianday1 < wifedeath.d_julianday2 AND" . " married.d_julianday1 <> 0" . " GROUP BY family" . " ORDER BY age {$age_dir}" ); $drows = $this->runSql( " SELECT DISTINCT fam.f_id AS family, MIN(divorced.d_julianday2-married.d_julianday1) AS age" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->id()}" . " WHERE" . " fam.f_file = {$this->tree->id()} AND" . " married.d_gid = fam.f_id AND" . " married.d_fact = 'MARR' AND" . " divorced.d_gid = fam.f_id AND" . " divorced.d_fact IN ('DIV', 'ANUL', '_SEPR', '_DETS') AND" . " married.d_julianday1 < divorced.d_julianday2 AND" . " married.d_julianday1 <> 0" . " GROUP BY family" . " ORDER BY age {$age_dir}" ); $rows = []; foreach ($drows as $family) { $rows[$family->family] = $family->age; } foreach ($hrows as $family) { if (!isset($rows[$family->family])) { $rows[$family->family] = $family->age; } } foreach ($wrows as $family) { if (!isset($rows[$family->family])) { $rows[$family->family] = $family->age; } elseif ($rows[$family->family] > $family->age) { $rows[$family->family] = $family->age; } } if ($age_dir === 'DESC') { arsort($rows); } else { asort($rows); } $top10 = []; $i = 0; foreach ($rows as $fam => $age) { $family = Family::getInstance($fam, $this->tree); if ($type === 'name') { return $family->formatList(); } $age = $this->calculateAge((int) $age); if ($type === 'age') { return $age; } $husb = $family->getHusband(); $wife = $family->getWife(); if (($husb && ($husb->getAllDeathDates() || !$husb->isDead())) && ($wife && ($wife->getAllDeathDates() || !$wife->isDead())) ) { if ($family && $family->canShow()) { if ($type === 'list') { $top10[] = '
  • ' . $family->getFullName() . ' (' . $age . ')' . '
  • '; } else { $top10[] = '' . $family->getFullName() . ' (' . $age . ')'; } } if (++$i === $total) { break; } } } if ($type === 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace([ '[', ']', '(', ')', '+', ], [ '‏[', '‏]', '‏(', '‏)', '‏+', ], $top10); } if ($type === 'list') { return ''; } return $top10; } /** * General query on marriage ages. * * @return string */ public function topAgeOfMarriageFamily(): string { return $this->ageOfMarriageQuery('name', 'DESC', 1); } /** * General query on marriage ages. * * @return string */ public function topAgeOfMarriage(): string { return $this->ageOfMarriageQuery('age', 'DESC', 1); } /** * General query on marriage ages. * * @param int $total * * @return string */ public function topAgeOfMarriageFamilies(int $total = 10): string { return $this->ageOfMarriageQuery('nolist', 'DESC', $total); } /** * General query on marriage ages. * * @param int $total * * @return string */ public function topAgeOfMarriageFamiliesList(int $total = 10): string { return $this->ageOfMarriageQuery('list', 'DESC', $total); } /** * General query on marriage ages. * * @return string */ public function minAgeOfMarriageFamily(): string { return $this->ageOfMarriageQuery('name', 'ASC', 1); } /** * General query on marriage ages. * * @return string */ public function minAgeOfMarriage(): string { return $this->ageOfMarriageQuery('age', 'ASC', 1); } /** * General query on marriage ages. * * @param int $total * * @return string */ public function minAgeOfMarriageFamilies(int $total = 10): string { return $this->ageOfMarriageQuery('nolist', 'ASC', $total); } /** * General query on marriage ages. * * @param int $total * * @return string */ public function minAgeOfMarriageFamiliesList(int $total = 10): string { return $this->ageOfMarriageQuery('list', 'ASC', $total); } /** * Find the ages between spouses. * * @param string $age_dir * @param int $total * * @return array */ private function ageBetweenSpousesQuery(string $age_dir, int $total): array { if ($age_dir === 'DESC') { $sql = "SELECT f_id AS xref, MIN(wife.d_julianday2-husb.d_julianday1) AS age" . " FROM `##families`" . " JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" . " JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" . " WHERE f_file = :tree_id" . " AND husb.d_fact = 'BIRT'" . " AND wife.d_fact = 'BIRT'" . " AND wife.d_julianday2 >= husb.d_julianday1 AND husb.d_julianday1 <> 0" . " GROUP BY xref" . " ORDER BY age DESC" . " LIMIT :limit"; } else { $sql = "SELECT f_id AS xref, MIN(husb.d_julianday2-wife.d_julianday1) AS age" . " FROM `##families`" . " JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" . " JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" . " WHERE f_file = :tree_id" . " AND husb.d_fact = 'BIRT'" . " AND wife.d_fact = 'BIRT'" . " AND husb.d_julianday2 >= wife.d_julianday1 AND wife.d_julianday1 <> 0" . " GROUP BY xref" . " ORDER BY age DESC" . " LIMIT :limit"; } $rows = Database::prepare( $sql )->execute([ 'tree_id' => $this->tree->id(), 'limit' => $total, ])->fetchAll(); $top10 = []; foreach ($rows as $fam) { $family = Family::getInstance($fam->xref, $this->tree); if ($fam->age < 0) { break; } if ($family->canShow()) { $top10[] = [ 'family' => $family, 'age' => $this->calculateAge((int) $fam->age), ]; } } return $top10; } /** * Find the age between husband and wife. * * @param int $total * * @return string */ public function ageBetweenSpousesMF(int $total = 10): string { $records = $this->ageBetweenSpousesQuery('DESC', $total); return view( 'statistics/families/top10-nolist-spouses', [ 'records' => $records, ] ); } /** * Find the age between husband and wife. * * @param int $total * * @return string */ public function ageBetweenSpousesMFList(int $total = 10): string { $records = $this->ageBetweenSpousesQuery('DESC', $total); return view( 'statistics/families/top10-list-spouses', [ 'records' => $records, ] ); } /** * Find the age between wife and husband.. * * @param int $total * * @return string */ public function ageBetweenSpousesFM(int $total = 10): string { $records = $this->ageBetweenSpousesQuery('ASC', $total); return view( 'statistics/families/top10-nolist-spouses', [ 'records' => $records, ] ); } /** * Find the age between wife and husband.. * * @param int $total * * @return string */ public function ageBetweenSpousesFMList(int $total = 10): string { $records = $this->ageBetweenSpousesQuery('ASC', $total); return view( 'statistics/families/top10-list-spouses', [ 'records' => $records, ] ); } /** * General query on ages at marriage. * * @param string $sex * @param int $year1 * @param int $year2 * * @return array */ public function statsMarrAgeQuery($sex = 'M', $year1 = -1, $year2 = -1): array { if ($year1 >= 0 && $year2 >= 0) { $years = " married.d_year BETWEEN {$year1} AND {$year2} AND "; } else { $years = ''; } $rows = $this->runSql( "SELECT " . " fam.f_id, " . " birth.d_gid, " . " married.d_julianday2-birth.d_julianday1 AS age " . "FROM `##dates` AS married " . "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " . "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " . "WHERE " . " '{$sex}' IN ('M', 'BOTH') AND {$years} " . " married.d_file={$this->tree->id()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " . " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " . "UNION ALL " . "SELECT " . " fam.f_id, " . " birth.d_gid, " . " married.d_julianday2-birth.d_julianday1 AS age " . "FROM `##dates` AS married " . "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " . "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " . "WHERE " . " '{$sex}' IN ('F', 'BOTH') AND {$years} " . " married.d_file={$this->tree->id()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " . " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " ); foreach ($rows as $row) { $row->age = (int) $row->age; } return $rows; } /** * General query on marriage ages. * * @param string $size * * @return string */ public function statsMarrAge(string $size = '200x250'): string { return (new ChartMarriageAge($this->tree)) ->chartMarriageAge($size); } /** * Query the database for marriage tags. * * @param string $type * @param string $age_dir * @param string $sex * @param bool $show_years * * @return string */ private function marriageQuery(string $type, string $age_dir, string $sex, bool $show_years): string { if ($sex === 'F') { $sex_field = 'f_wife'; } else { $sex_field = 'f_husb'; } if ($age_dir !== 'ASC') { $age_dir = 'DESC'; } $rows = $this->runSql( " SELECT fam.f_id AS famid, fam.{$sex_field}, married.d_julianday2-birth.d_julianday1 AS age, indi.i_id AS i_id" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS birth ON birth.d_file = {$this->tree->id()}" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . " WHERE" . " birth.d_gid = indi.i_id AND" . " married.d_gid = fam.f_id AND" . " indi.i_id = fam.{$sex_field} AND" . " fam.f_file = {$this->tree->id()} AND" . " birth.d_fact = 'BIRT' AND" . " married.d_fact = 'MARR' AND" . " birth.d_julianday1 <> 0 AND" . " married.d_julianday2 > birth.d_julianday1 AND" . " i_sex='{$sex}'" . " ORDER BY" . " married.d_julianday2-birth.d_julianday1 {$age_dir} LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; if (isset($row->famid)) { $family = Family::getInstance($row->famid, $this->tree); } if (isset($row->i_id)) { $person = Individual::getInstance($row->i_id, $this->tree); } switch ($type) { default: case 'full': if ($family && $family->canShow()) { $result = $family->formatList(); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'name': $result = '' . $person->getFullName() . ''; break; case 'age': $age = $row->age; if ($show_years) { $result = $this->calculateAge((int) $row->age); } else { $result = I18N::number((int) ($age / 365.25)); } break; } return $result; } /** * Find the youngest wife. * * @return string */ public function youngestMarriageFemale(): string { return $this->marriageQuery('full', 'ASC', 'F', false); } /** * Find the name of the youngest wife. * * @return string */ public function youngestMarriageFemaleName(): string { return $this->marriageQuery('name', 'ASC', 'F', false); } /** * Find the age of the youngest wife. * * @param string $show_years * * @return string */ public function youngestMarriageFemaleAge(string $show_years = ''): string { return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years); } /** * Find the oldest wife. * * @return string */ public function oldestMarriageFemale(): string { return $this->marriageQuery('full', 'DESC', 'F', false); } /** * Find the name of the oldest wife. * * @return string */ public function oldestMarriageFemaleName(): string { return $this->marriageQuery('name', 'DESC', 'F', false); } /** * Find the age of the oldest wife. * * @param string $show_years * * @return string */ public function oldestMarriageFemaleAge(string $show_years = ''): string { return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years); } /** * Find the youngest husband. * * @return string */ public function youngestMarriageMale(): string { return $this->marriageQuery('full', 'ASC', 'M', false); } /** * Find the name of the youngest husband. * * @return string */ public function youngestMarriageMaleName(): string { return $this->marriageQuery('name', 'ASC', 'M', false); } /** * Find the age of the youngest husband. * * @param string $show_years * * @return string */ public function youngestMarriageMaleAge(string $show_years = ''): string { return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years); } /** * Find the oldest husband. * * @return string */ public function oldestMarriageMale(): string { return $this->marriageQuery('full', 'DESC', 'M', false); } /** * Find the name of the oldest husband. * * @return string */ public function oldestMarriageMaleName(): string { return $this->marriageQuery('name', 'DESC', 'M', false); } /** * Find the age of the oldest husband. * * @param string $show_years * * @return string */ public function oldestMarriageMaleAge(string $show_years = ''): string { return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years); } /** * General query on marriages. * * @param bool $first * @param int $year1 * @param int $year2 * * @return array */ public function statsMarrQuery(bool $first = false, int $year1 = -1, int $year2 = -1): array { if ($first) { $years = ''; if ($year1 >= 0 && $year2 >= 0) { $years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND"; } $sql = " SELECT fam.f_id AS fams, fam.f_husb, fam.f_wife, married.d_julianday2 AS age, married.d_month AS month, indi.i_id AS indi" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . " WHERE" . " married.d_gid = fam.f_id AND" . " fam.f_file = {$this->tree->id()} AND" . " married.d_fact = 'MARR' AND" . " married.d_julianday2 <> 0 AND" . $years . " (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" . " ORDER BY fams, indi, age ASC"; } else { $sql = "SELECT d_month, COUNT(*) AS total" . " FROM `##dates`" . " WHERE d_file={$this->tree->id()} AND d_fact='MARR'"; if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } $sql .= " GROUP BY d_month"; } return $this->runSql($sql); } /** * General query on marriages. * * @param string|null $size * @param string|null $color_from * @param string|null $color_to * * @return string */ public function statsMarr(string $size = null, string $color_from = null, string $color_to = null): string { return (new ChartMarriage($this->tree)) ->chartMarriage($size, $color_from, $color_to); } /** * General divorce query. * * @param string|null $size * @param string|null $color_from * @param string|null $color_to * * @return string */ public function statsDiv(string $size = null, string $color_from = null, string $color_to = null): string { return (new ChartDivorce($this->tree)) ->chartDivorce($size, $color_from, $color_to); } }