1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 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 <http://www.gnu.org/licenses/>. 16 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees; 20 21use function implode; 22use function preg_match; 23use function strtolower; 24use function substr; 25use function trim; 26 27/** 28 * Representation of a GEDCOM age. 29 * 30 * Ages may be a keyword (stillborn, infant, child) or a number of years, 31 * months and/or days such as "6y 3m". 32 */ 33class Age 34{ 35 // GEDCOM keyword: died just prior, at, or near birth, 0 years 36 private const KEYWORD_STILLBORN = 'stillborn'; 37 38 // GEDCOM keyword: age < 1 year 39 private const KEYWORD_INFANT = 'infant'; 40 41 // GEDCOM keyword: age < 8 years 42 private const KEYWORD_CHILD = 'child'; 43 44 // GEDCOM symbol: aged less than 45 private const SYMBOL_LESS_THAN = '<'; 46 47 // GEDCOM symbol: aged more than 48 private const SYMBOL_MORE_THAN = '>'; 49 50 // GEDCOM symbol: number of years 51 private const SYMBOL_YEARS = 'y'; 52 53 // GEDCOM symbol: number of months 54 private const SYMBOL_MONTHS = 'm'; 55 56 // GEDCOM symbol: number of weeks (this is a non-standard extension) 57 private const SYMBOL_WEEKS = 'w'; 58 59 // GEDCOM symbol: number of days 60 private const SYMBOL_DAYS = 'd'; 61 62 /** @var string */ 63 private $keyword = ''; 64 65 /** @var string */ 66 private $qualifier = ''; 67 68 /** @var int */ 69 private $years = 0; 70 71 /** @var int */ 72 private $months = 0; 73 74 /** @var int */ 75 private $weeks = 0; 76 77 /** @var int */ 78 private $days = 0; 79 80 /** 81 * Age constructor. 82 * 83 * @param string $age 84 */ 85 public function __construct(string $age) 86 { 87 $age = strtolower(trim($age)); 88 89 // Keywords 90 if ($age === self::KEYWORD_STILLBORN || $age === self::KEYWORD_INFANT || $age === self::KEYWORD_CHILD) { 91 $this->keyword = $age; 92 93 return; 94 } 95 96 // Qualifier 97 $qualifier = substr($age, 0, 1); 98 99 if ($qualifier === self::SYMBOL_LESS_THAN || $qualifier === self::SYMBOL_MORE_THAN) { 100 $this->qualifier = $qualifier; 101 } 102 103 // Number of years, months, weeks and days. 104 $this->years = $this->extractNumber($age, self::SYMBOL_YEARS); 105 $this->months = $this->extractNumber($age, self::SYMBOL_MONTHS); 106 $this->weeks = $this->extractNumber($age, self::SYMBOL_WEEKS); 107 $this->days = $this->extractNumber($age, self::SYMBOL_DAYS); 108 } 109 110 /** 111 * Convert an age to localised text. 112 */ 113 public function asText(): string 114 { 115 if ($this->keyword === self::KEYWORD_STILLBORN) { 116 // I18N: An individual’s age at an event. e.g. Died 14 Jan 1900 (stillborn) 117 return I18N::translate('(stillborn)'); 118 } 119 120 if ($this->keyword === self::KEYWORD_INFANT) { 121 // I18N: An individual’s age at an event. e.g. Died 14 Jan 1900 (in infancy) 122 return I18N::translate('(in infancy)'); 123 } 124 125 if ($this->keyword === self::KEYWORD_CHILD) { 126 // I18N: An individual’s age at an event. e.g. Died 14 Jan 1900 (in childhood) 127 return I18N::translate('(in childhood)'); 128 } 129 130 $age = []; 131 132 // Show a zero age as "0 years", not "0 days" 133 if ($this->years > 0 || $this->months === 0 && $this->weeks === 0 && $this->days === 0) { 134 // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 135 $age[] = I18N::plural('%s year', '%s years', $this->years, I18N::number($this->years)); 136 } 137 138 if ($this->months > 0) { 139 // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 140 $age[] = I18N::plural('%s month', '%s months', $this->months, I18N::number($this->months)); 141 } 142 143 if ($this->weeks > 0) { 144 // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 145 $age[] = I18N::plural('%s week', '%s weeks', $this->weeks, I18N::number($this->weeks)); 146 } 147 148 if ($this->days > 0) { 149 // I18N: Part of an age string. e.g. 5 years, 4 months and 3 days 150 $age[] = I18N::plural('%s day', '%s days', $this->days, I18N::number($this->days)); 151 } 152 153 // If an age is just a number of years, only show the number 154 if ($this->years > 0 && $this->months === 0 && $this->weeks === 0 && $this->days === 0) { 155 $age = [I18N::number($this->years)]; 156 } 157 158 $age_string = implode(I18N::$list_separator, $age); 159 160 if ($this->qualifier === self::SYMBOL_LESS_THAN) { 161 // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged less than 21 years) 162 return I18N::translate('(aged less than %s)', $age_string); 163 } 164 165 if ($this->qualifier === self::SYMBOL_MORE_THAN) { 166 // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged more than 21 years) 167 return I18N::translate('(aged more than %s)', $age_string); 168 } 169 170 // I18N: Description of an individual’s age at an event. For example, Died 14 Jan 1900 (aged 43 years) 171 return I18N::translate('(aged %s)', $age_string); 172 } 173 174 /** 175 * Extract a number of days/weeks/months/years from the age string. 176 * 177 * @param string $age 178 * @param string $suffix 179 * 180 * @return int 181 */ 182 private function extractNumber(string $age, string $suffix): int 183 { 184 if (preg_match('/(\d+) *' . $suffix . '/', $age, $match)) { 185 return (int) $match[1]; 186 } 187 188 return 0; 189 } 190} 191