1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2018 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Services; 19 20use Fisharebest\Webtrees\Database; 21use Fisharebest\Webtrees\Date; 22use Fisharebest\Webtrees\Date\CalendarDate; 23use Fisharebest\Webtrees\Date\FrenchDate; 24use Fisharebest\Webtrees\Date\GregorianDate; 25use Fisharebest\Webtrees\Date\HijriDate; 26use Fisharebest\Webtrees\Date\JalaliDate; 27use Fisharebest\Webtrees\Date\JewishDate; 28use Fisharebest\Webtrees\Date\JulianDate; 29use Fisharebest\Webtrees\Fact; 30use Fisharebest\Webtrees\Family; 31use Fisharebest\Webtrees\GedcomRecord; 32use Fisharebest\Webtrees\Individual; 33use Fisharebest\Webtrees\Tree; 34 35/** 36 * Calculate anniversaries, etc. 37 */ 38class CalendarService 39{ 40 /** 41 * List all the months in a given year. 42 * 43 * @param string $calendar 44 * @param int $year 45 * 46 * @return string[] 47 */ 48 public function calendarMonthsInYear(string $calendar, int $year): array 49 { 50 $date = new Date($calendar . ' ' . $year); 51 $calendar_date = $date->minimumDate(); 52 $month_numbers = range(1, $calendar_date->monthsInYear()); 53 $month_names = []; 54 55 foreach ($month_numbers as $month_number) { 56 $calendar_date->d = 1; 57 $calendar_date->m = $month_number; 58 $calendar_date->setJdFromYmd(); 59 60 if ($month_number === 6 && $calendar_date instanceof JewishDate && !$calendar_date->isLeapYear()) { 61 // No month 6 in Jewish non-leap years. 62 continue; 63 } 64 65 if ($month_number === 7 && $calendar_date instanceof JewishDate && !$calendar_date->isLeapYear()) { 66 // Month 7 is ADR in Jewish non-leap years (and ADS in others). 67 $mon = 'ADR'; 68 } else { 69 $mon = $calendar_date->format('%O'); 70 } 71 72 73 $month_names[$mon] = $calendar_date->format('%F'); 74 } 75 76 return $month_names; 77 } 78 79 /** 80 * Get a list of events which occured during a given date range. 81 * 82 * @param int $jd1 the start range of julian day 83 * @param int $jd2 the end range of julian day 84 * @param string $facts restrict the search to just these facts or leave blank for all 85 * @param Tree $tree the tree to search 86 * 87 * @return Fact[] 88 */ 89 public function getCalendarEvents(int $jd1, int $jd2, string $facts, Tree $tree): array 90 { 91 // If no facts specified, get all except these 92 $skipfacts = 'CHAN,BAPL,SLGC,SLGS,ENDL,CENS,RESI,NOTE,ADDR,OBJE,SOUR'; 93 94 $found_facts = []; 95 96 // Events that start or end during the period 97 $where = "WHERE (d_julianday1>={$jd1} AND d_julianday1<={$jd2} OR d_julianday2>={$jd1} AND d_julianday2<={$jd2})"; 98 99 // Restrict to certain types of fact 100 if (empty($facts)) { 101 $excl_facts = "'" . preg_replace('/\W+/', "','", $skipfacts) . "'"; 102 $where .= " AND d_fact NOT IN ({$excl_facts})"; 103 } else { 104 $incl_facts = "'" . preg_replace('/\W+/', "','", $facts) . "'"; 105 $where .= " AND d_fact IN ({$incl_facts})"; 106 } 107 // Only get events from the current gedcom 108 $where .= " AND d_file=" . $tree->getTreeId(); 109 110 // Now fetch these events 111 $ind_sql = "SELECT d_gid AS xref, i_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact, d_type FROM `##dates`, `##individuals` {$where} AND d_gid=i_id AND d_file=i_file ORDER BY d_julianday1"; 112 $fam_sql = "SELECT d_gid AS xref, f_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact, d_type FROM `##dates`, `##families` {$where} AND d_gid=f_id AND d_file=f_file ORDER BY d_julianday1"; 113 114 foreach (['INDI' => $ind_sql, 'FAM' => $fam_sql] as $type => $sql) { 115 $rows = Database::prepare($sql)->fetchAll(); 116 117 foreach ($rows as $row) { 118 if ($type === 'INDI') { 119 $record = Individual::getInstance($row->xref, $tree, $row->gedcom); 120 } else { 121 $record = Family::getInstance($row->xref, $tree, $row->gedcom); 122 } 123 $anniv_date = new Date($row->d_type . ' ' . $row->d_day . ' ' . $row->d_month . ' ' . $row->d_year); 124 foreach ($record->getFacts() as $fact) { 125 // For date ranges, we need a match on either the start/end. 126 if (($fact->getDate()->minimumJulianDay() === $anniv_date->minimumJulianDay() || $fact->getDate()->maximumJulianDay() == $anniv_date->maximumJulianDay()) && $fact->getTag() === $row->d_fact) { 127 $fact->anniv = 0; 128 $found_facts[] = $fact; 129 } 130 } 131 } 132 } 133 134 return $found_facts; 135 } 136 137 /** 138 * Get the list of current and upcoming events, sorted by anniversary date 139 * 140 * @param int $jd1 141 * @param int $jd2 142 * @param string $events 143 * @param bool $only_living 144 * @param string $sort_by 145 * @param Tree $tree 146 * 147 * @return Fact[] 148 */ 149 public function getEventsList(int $jd1, int $jd2, string $events, bool $only_living, string $sort_by, Tree $tree): array 150 { 151 $found_facts = []; 152 $facts = []; 153 154 foreach (range($jd1, $jd2) as $jd) { 155 $found_facts = array_merge($found_facts, $this->getAnniversaryEvents($jd, $events, $tree)); 156 } 157 158 foreach ($found_facts as $fact) { 159 $record = $fact->getParent(); 160 // only living people ? 161 if ($only_living) { 162 if ($record instanceof Individual && $record->isDead()) { 163 continue; 164 } 165 if ($record instanceof Family) { 166 $husb = $record->getHusband(); 167 if ($husb === null || $husb->isDead()) { 168 continue; 169 } 170 $wife = $record->getWife(); 171 if ($wife === null || $wife->isDead()) { 172 continue; 173 } 174 } 175 } 176 $facts[] = $fact; 177 } 178 179 switch ($sort_by) { 180 case 'anniv': 181 uasort($facts, function (Fact $x, Fact $y): int { 182 return Fact::compareDate($y, $x); 183 }); 184 break; 185 case 'alpha': 186 uasort($facts, function (Fact $x, Fact $y): int { 187 return GedcomRecord::compare($x->getParent(), $y->getParent()); 188 }); 189 break; 190 } 191 192 return $facts; 193 } 194 195 /** 196 * Get a list of events whose anniversary occured on a given julian day. 197 * Used on the on-this-day/upcoming blocks and the day/month calendar views. 198 * 199 * @param int $jd the julian day 200 * @param string $facts restrict the search to just these facts or leave blank for all 201 * @param Tree $tree the tree to search 202 * 203 * @return Fact[] 204 */ 205 public function getAnniversaryEvents($jd, $facts, Tree $tree): array 206 { 207 $found_facts = []; 208 209 $anniversaries = [ 210 new GregorianDate($jd), 211 new JulianDate($jd), 212 new FrenchDate($jd), 213 new JewishDate($jd), 214 new HijriDate($jd), 215 new JalaliDate($jd), 216 ]; 217 218 foreach ($anniversaries as $anniv) { 219 // Build a SQL where clause to match anniversaries in the appropriate calendar. 220 $ind_sql = 221 "SELECT DISTINCT i_id AS xref, i_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact" . 222 " FROM `##dates` JOIN `##individuals` ON d_gid = i_id AND d_file = i_file" . 223 " WHERE d_type = :type AND d_file = :tree_id"; 224 $fam_sql = 225 "SELECT DISTINCT f_id AS xref, f_gedcom AS gedcom, d_type, d_day, d_month, d_year, d_fact" . 226 " FROM `##dates` JOIN `##families` ON d_gid = f_id AND d_file = f_file" . 227 " WHERE d_type = :type AND d_file = :tree_id"; 228 $args = [ 229 'type' => $anniv->format('%@'), 230 'tree_id' => $tree->getTreeId(), 231 ]; 232 233 $where = ""; 234 // SIMPLE CASES: 235 // a) Non-hebrew anniversaries 236 // b) Hebrew months TVT, SHV, IYR, SVN, TMZ, AAV, ELL 237 if (!$anniv instanceof JewishDate || in_array($anniv->m, [ 238 1, 239 5, 240 6, 241 9, 242 10, 243 11, 244 12, 245 13, 246 ])) { 247 // Dates without days go on the first day of the month 248 // Dates with invalid days go on the last day of the month 249 if ($anniv->d === 1) { 250 $where .= " AND d_day <= 1"; 251 } elseif ($anniv->d === $anniv->daysInMonth()) { 252 $where .= " AND d_day >= :day"; 253 $args['day'] = $anniv->d; 254 } else { 255 $where .= " AND d_day = :day"; 256 $args['day'] = $anniv->d; 257 } 258 $where .= " AND d_mon = :month"; 259 $args['month'] = $anniv->m; 260 } else { 261 // SPECIAL CASES: 262 switch ($anniv->m) { 263 case 2: 264 // 29 CSH does not include 30 CSH (but would include an invalid 31 CSH if there were no 30 CSH) 265 if ($anniv->d === 1) { 266 $where .= " AND d_day <= 1 AND d_mon = 2"; 267 } elseif ($anniv->d === 30) { 268 $where .= " AND d_day >= 30 AND d_mon = 2"; 269 } elseif ($anniv->d === 29 && $anniv->daysInMonth() === 29) { 270 $where .= " AND (d_day = 29 OR d_day > 30) AND d_mon = 2"; 271 } else { 272 $where .= " AND d_day = :day AND d_mon = 2"; 273 $args['day'] = $anniv->d; 274 } 275 break; 276 case 3: 277 // 1 KSL includes 30 CSH (if this year didn’t have 30 CSH) 278 // 29 KSL does not include 30 KSL (but would include an invalid 31 KSL if there were no 30 KSL) 279 if ($anniv->d === 1) { 280 $tmp = new JewishDate([ 281 $anniv->y, 282 'CSH', 283 1, 284 ]); 285 if ($tmp->daysInMonth() === 29) { 286 $where .= " AND (d_day <= 1 AND d_mon = 3 OR d_day = 30 AND d_mon = 2)"; 287 } else { 288 $where .= " AND d_day <= 1 AND d_mon = 3"; 289 } 290 } elseif ($anniv->d === 30) { 291 $where .= " AND d_day >= 30 AND d_mon = 3"; 292 } elseif ($anniv->d == 29 && $anniv->daysInMonth() === 29) { 293 $where .= " AND (d_day = 29 OR d_day > 30) AND d_mon = 3"; 294 } else { 295 $where .= " AND d_day = :day AND d_mon = 3"; 296 $args['day'] = $anniv->d; 297 } 298 break; 299 case 4: 300 // 1 TVT includes 30 KSL (if this year didn’t have 30 KSL) 301 if ($anniv->d === 1) { 302 $tmp = new JewishDate([ 303 $anniv->y, 304 'KSL', 305 1, 306 ]); 307 if ($tmp->daysInMonth() === 29) { 308 $where .= " AND (d_day <=1 AND d_mon = 4 OR d_day = 30 AND d_mon = 3)"; 309 } else { 310 $where .= " AND d_day <= 1 AND d_mon = 4"; 311 } 312 } elseif ($anniv->d === $anniv->daysInMonth()) { 313 $where .= " AND d_day >= :day AND d_mon=4"; 314 $args['day'] = $anniv->d; 315 } else { 316 $where .= " AND d_day = :day AND d_mon=4"; 317 $args['day'] = $anniv->d; 318 } 319 break; 320 case 7: // ADS includes ADR (non-leap) 321 if ($anniv->d === 1) { 322 $where .= " AND d_day <= 1"; 323 } elseif ($anniv->d === $anniv->daysInMonth()) { 324 $where .= " AND d_day >= :day"; 325 $args['day'] = $anniv->d; 326 } else { 327 $where .= " AND d_day = :day"; 328 $args['day'] = $anniv->d; 329 } 330 $where .= " AND (d_mon = 6 AND MOD(7 * d_year + 1, 19) >= 7 OR d_mon = 7)"; 331 break; 332 case 8: // 1 NSN includes 30 ADR, if this year is non-leap 333 if ($anniv->d === 1) { 334 if ($anniv->isLeapYear()) { 335 $where .= " AND d_day <= 1 AND d_mon = 8"; 336 } else { 337 $where .= " AND (d_day <= 1 AND d_mon = 8 OR d_day = 30 AND d_mon = 6)"; 338 } 339 } elseif ($anniv->d === $anniv->daysInMonth()) { 340 $where .= " AND d_day >= :day AND d_mon = 8"; 341 $args['day'] = $anniv->d; 342 } else { 343 $where .= " AND d_day = :day AND d_mon = 8"; 344 $args['day'] = $anniv->d; 345 } 346 break; 347 } 348 } 349 // Only events in the past (includes dates without a year) 350 $where .= " AND d_year <= :year"; 351 $args['year'] = $anniv->y; 352 353 if ($facts) { 354 // Restrict to certain types of fact 355 $where .= " AND d_fact IN ("; 356 preg_match_all('/([_A-Z]+)/', $facts, $matches); 357 foreach ($matches[1] as $n => $fact) { 358 $where .= $n ? ", " : ""; 359 $where .= ":fact_" . $n; 360 $args['fact_' . $n] = $fact; 361 } 362 $where .= ")"; 363 } else { 364 // If no facts specified, get all except these 365 $where .= " AND d_fact NOT IN ('CHAN', 'BAPL', 'SLGC', 'SLGS', 'ENDL', 'CENS', 'RESI', '_TODO')"; 366 } 367 368 $order_by = " ORDER BY d_day, d_year DESC"; 369 370 // Now fetch these anniversaries 371 foreach ([ 372 'INDI' => $ind_sql . $where . $order_by, 373 'FAM' => $fam_sql . $where . $order_by, 374 ] as $type => $sql) { 375 $rows = Database::prepare($sql)->execute($args)->fetchAll(); 376 foreach ($rows as $row) { 377 if ($type === 'INDI') { 378 $record = Individual::getInstance($row->xref, $tree, $row->gedcom); 379 } else { 380 $record = Family::getInstance($row->xref, $tree, $row->gedcom); 381 } 382 $anniv_date = new Date($row->d_type . ' ' . $row->d_day . ' ' . $row->d_month . ' ' . $row->d_year); 383 foreach ($record->getFacts() as $fact) { 384 if (($fact->getDate()->minimumJulianDay() === $anniv_date->minimumJulianDay() || $fact->getDate()->maximumJulianDay() === $anniv_date->maximumJulianDay()) && $fact->getTag() === $row->d_fact) { 385 $fact->anniv = $row->d_year === '0' ? 0 : $anniv->y - $row->d_year; 386 $fact->jd = $jd; 387 $found_facts[] = $fact; 388 } 389 } 390 } 391 } 392 } 393 394 return $found_facts; 395 } 396} 397