ISO 3166-1 alpha-3 to alpha-2 */ private const ALPHA3_TO_ALPHA2 = [ 'AFG' => 'AF', 'ALA' => 'AX', 'ALB' => 'AL', 'DZA' => 'DZ', 'ASM' => 'AS', 'AND' => 'AD', 'AGO' => 'AO', 'AIA' => 'AI', 'ATG' => 'AG', 'ARG' => 'AR', 'ARM' => 'AM', 'ABW' => 'AW', 'AUS' => 'AU', 'AUT' => 'AT', 'AZE' => 'AZ', 'BHS' => 'BS', 'BHR' => 'BH', 'BGD' => 'BD', 'BRB' => 'BB', 'BLR' => 'BY', 'BEL' => 'BE', 'BLZ' => 'BZ', 'BEN' => 'BJ', 'BMU' => 'BM', 'BTN' => 'BT', 'BOL' => 'BO', 'BIH' => 'BA', 'BWA' => 'BW', 'BRA' => 'BR', 'VGB' => 'VG', 'BRN' => 'BN', 'BGR' => 'BG', 'BFA' => 'BF', 'BDI' => 'BI', 'KHM' => 'KH', 'CMR' => 'CM', 'CAN' => 'CA', 'CPV' => 'CV', 'CYM' => 'KY', 'CAF' => 'CF', 'TCD' => 'TD', 'CHL' => 'CL', 'CHN' => 'CN', 'COL' => 'CO', 'COM' => 'KM', 'COG' => 'CG', 'COD' => 'CD', 'COK' => 'CK', 'CRI' => 'CR', 'CIV' => 'CI', 'HRV' => 'HR', 'CUB' => 'CU', 'CUW' => 'CW', 'CYP' => 'CY', 'CZE' => 'CZ', 'DNK' => 'DK', 'DJI' => 'DJ', 'DMA' => 'DM', 'DOM' => 'DO', 'ECU' => 'EC', 'EGY' => 'EG', 'SLV' => 'SV', 'GNQ' => 'GQ', 'ERI' => 'ER', 'EST' => 'EE', 'SWZ' => 'SZ', 'ETH' => 'ET', 'FRO' => 'FO', 'FJI' => 'FJ', 'FIN' => 'FI', 'FRA' => 'FR', 'GUF' => 'GF', 'PYF' => 'PF', 'GAB' => 'GA', 'GMB' => 'GM', 'GEO' => 'GE', 'DEU' => 'DE', 'GHA' => 'GH', 'GIB' => 'GI', 'GRC' => 'GR', 'GRL' => 'GL', 'GRD' => 'GD', 'GLP' => 'GP', 'GUM' => 'GU', 'GTM' => 'GT', 'GGY' => 'GG', 'GIN' => 'GN', 'GNB' => 'GW', 'GUY' => 'GY', 'HTI' => 'HT', 'HND' => 'HN', 'HKG' => 'HK', 'HUN' => 'HU', 'ISL' => 'IS', 'IND' => 'IN', 'IDN' => 'ID', 'IRN' => 'IR', 'IRQ' => 'IQ', 'IRL' => 'IE', 'IMN' => 'IM', 'ISR' => 'IL', 'ITA' => 'IT', 'JAM' => 'JM', 'JPN' => 'JP', 'JEY' => 'JE', 'JOR' => 'JO', 'KAZ' => 'KZ', 'KEN' => 'KE', 'KIR' => 'KI', 'XKX' => 'XK', 'KWT' => 'KW', 'KGZ' => 'KG', 'LAO' => 'LA', 'LVA' => 'LV', 'LBN' => 'LB', 'LSO' => 'LS', 'LBR' => 'LR', 'LBY' => 'LY', 'LIE' => 'LI', 'LTU' => 'LT', 'LUX' => 'LU', 'MAC' => 'MO', 'MDG' => 'MG', 'MWI' => 'MW', 'MYS' => 'MY', 'MDV' => 'MV', 'MLI' => 'ML', 'MLT' => 'MT', 'MHL' => 'MH', 'MTQ' => 'MQ', 'MRT' => 'MR', 'MUS' => 'MU', 'MYT' => 'YT', 'MEX' => 'MX', 'FSM' => 'FM', 'MDA' => 'MD', 'MCO' => 'MC', 'MNG' => 'MN', 'MNE' => 'ME', 'MSR' => 'MS', 'MAR' => 'MA', 'MOZ' => 'MZ', 'MMR' => 'MM', 'NAM' => 'NA', 'NRU' => 'NR', 'NPL' => 'NP', 'NLD' => 'NL', 'NCL' => 'NC', 'NZL' => 'NZ', 'NIC' => 'NI', 'NER' => 'NE', 'NGA' => 'NG', 'NIU' => 'NU', 'PRK' => 'KP', 'MKD' => 'MK', 'MNP' => 'MP', 'NOR' => 'NO', 'OMN' => 'OM', 'PAK' => 'PK', 'PLW' => 'PW', 'PSE' => 'PS', 'PAN' => 'PA', 'PNG' => 'PG', 'PRY' => 'PY', 'PER' => 'PE', 'PHL' => 'PH', 'POL' => 'PL', 'PRT' => 'PT', 'PRI' => 'PR', 'QAT' => 'QA', 'REU' => 'RE', 'ROU' => 'RO', 'RUS' => 'RU', 'RWA' => 'RW', 'WSM' => 'WS', 'SMR' => 'SM', 'STP' => 'ST', 'SAU' => 'SA', 'SEN' => 'SN', 'SRB' => 'RS', 'SYC' => 'SC', 'SLE' => 'SL', 'SGP' => 'SG', 'SXM' => 'SX', 'SVK' => 'SK', 'SVN' => 'SI', 'SLB' => 'SB', 'SOM' => 'SO', 'ZAF' => 'ZA', 'KOR' => 'KR', 'SSD' => 'SS', 'ESP' => 'ES', 'LKA' => 'LK', 'KNA' => 'KN', 'LCA' => 'LC', 'VCT' => 'VC', 'SDN' => 'SD', 'SUR' => 'SR', 'SWE' => 'SE', 'CHE' => 'CH', 'SYR' => 'SY', 'TWN' => 'TW', 'TJK' => 'TJ', 'TZA' => 'TZ', 'THA' => 'TH', 'TLS' => 'TL', 'TGO' => 'TG', 'TON' => 'TO', 'TTO' => 'TT', 'TUN' => 'TN', 'TUR' => 'TR', 'TKM' => 'TM', 'TCA' => 'TC', 'TUV' => 'TV', 'UGA' => 'UG', 'UKR' => 'UA', 'ARE' => 'AE', 'GBR' => 'GB', 'USA' => 'US', 'VIR' => 'VI', 'URY' => 'UY', 'UZB' => 'UZ', 'VUT' => 'VU', 'VAT' => 'VA', 'VEN' => 'VE', 'VNM' => 'VN', 'WLF' => 'WF', 'YEM' => 'YE', 'ZMB' => 'ZM', 'ZWE' => 'ZW', ]; /** @var array */ private const COUNTRY_ALIASES = [ 'UK' => 'GB', 'UNITED KINGDOM' => 'GB', 'GREAT BRITAIN' => 'GB', 'ENGLAND' => 'GB', 'SCOTLAND' => 'GB', 'WALES' => 'GB', 'NORTHERN IRELAND' => 'GB', 'UNITED STATES' => 'US', 'IRELAND' => 'IE', ]; /** * Resolve an ISO alpha-2 code from an address, accepting alpha-2, alpha-3 or common country names. * * @param array $address */ public function countryCode(array $address): ?string { foreach (['CountryIsoCode', 'CountryISOCode', 'CountryIso3Code', 'CountryISO3Code', 'CountryIso', 'Country'] as $field) { $value = strtoupper(trim((string) ($address[$field] ?? ''))); if ($value === '') { continue; } if (isset(self::COUNTRY_ALIASES[$value])) { return self::COUNTRY_ALIASES[$value]; } if (strlen($value) === 3 && isset(self::ALPHA3_TO_ALPHA2[$value])) { return self::ALPHA3_TO_ALPHA2[$value]; } if (preg_match('/^[A-Z]{2}$/', $value) === 1) { return $value; } } return null; } /** * Render a small flag image for the address destination, or a dash when the country is unknown. * * @param array $address */ public function flag(array $address): HtmlString { $code = $this->countryCode($address); if ($code === null) { return new HtmlString('—'); } $lower = strtolower($code); return new HtmlString(sprintf( '%2$s flag%2$s', $lower, e($code), )); } /** Format a number of seconds as a short duration such as "2h 1m". */ public function duration(int|float|null $seconds): string { if ($seconds === null || $seconds <= 0) { return '—'; } $minutes = (int) round($seconds / 60); return $minutes >= 60 ? intdiv($minutes, 60).'h '.($minutes % 60).'m' : $minutes.'m'; } /** Format an API date or date-time as a readable string, dropping the time when it is midnight. */ public function dateTime(string|int|null $value): string { if ($value === null || $value === '') { return '—'; } try { $date = is_int($value) ? Carbon::createFromTimestamp($value) : Carbon::parse($value); } catch (Throwable) { return (string) $value; } return $date->format($date->format('H:i:s') === '00:00:00' ? 'D j M Y' : 'D j M Y, H:i'); } }