.
*/
namespace Gibbon\Services;
use DateTime;
use DateTimeImmutable;
use Gibbon\Http\Url;
use Gibbon\Contracts\Services\Session;
/**
* Format values based on locale and system settings.
*
* @version v16
* @since v16
*/
class Format
{
use FormatResolver;
public const NONE = -1;
public const FULL = 0;
public const LONG = 1;
public const MEDIUM = 2;
public const SHORT = 3;
public const FULL_NO_YEAR = 100;
public const LONG_NO_YEAR = 101;
public const MEDIUM_NO_YEAR = 102;
public const SHORT_NO_YEAR = 103;
protected static $settings = [
'dateFormatPHP' => 'd/m/Y',
'dateTimeFormatPHP' => 'd/m/Y H:i',
'timeFormatPHP' => 'H:i',
'dateFormatFull' => 'l, F j',
'dateFormatLong' => 'F j',
'dateFormatMedium' => 'M j',
'dateFormatIntlFull' => 'EEEE, d MMMM',
'dateFormatIntlLong' => 'd MMMM',
'dateFormatIntlMedium' => 'd MMM',
'dateFormatGenerate' => true,
];
public static $intlFormatterAvailable = false;
/**
* Sets the internal formatting options from an array.
*
* @param array $settings
*/
public static function setup(array $settings)
{
static::$settings = array_replace(static::$settings, $settings);
static::$intlFormatterAvailable = class_exists('IntlDateFormatter');
// Generate best-fit date formats for this locale, if possible
if (static::$settings['dateFormatGenerate'] && class_exists('IntlDatePatternGenerator')) {
$intlPatternGenerator = new \IntlDatePatternGenerator(static::$settings['code']);
static::$settings['dateFormatIntlFull'] = $intlPatternGenerator->getBestPattern('EEEEMMMMd');
static::$settings['dateFormatIntlLong'] = $intlPatternGenerator->getBestPattern('MMMMd');
static::$settings['dateFormatIntlMedium'] = $intlPatternGenerator->getBestPattern('MMMd');
} else {
static::$settings['dateFormatIntlFull'] = static::$settings['code'] == 'en_GB' ? 'EEEE, d MMMM' : 'EEEE, MMMM d';
static::$settings['dateFormatIntlLong'] = static::$settings['code'] == 'en_GB' ? 'd MMMM' : 'MMMM d';
static::$settings['dateFormatIntlMedium'] = static::$settings['code'] == 'en_GB' ? 'd MMM' : 'MMM d';
}
}
/**
* Sets the formatting options from session i18n and database settings.
*
* @param Session $session
*/
public static function setupFromSession(Session $session)
{
$settings = $session->get('i18n');
$settings['absolutePath'] = $session->get('absolutePath');
$settings['absoluteURL'] = $session->get('absoluteURL');
$settings['gibbonThemeName'] = $session->get('gibbonThemeName');
$settings['currency'] = $session->get('currency') ?? '';
$settings['currencySymbol'] = !empty(substr($settings['currency'], 4)) ? substr($settings['currency'], 4) : '';
$settings['currencyName'] = substr($settings['currency'], 0, 3);
$settings['nameFormatStaffInformal'] = $session->get('nameFormatStaffInformal');
$settings['nameFormatStaffInformalReversed'] = $session->get('nameFormatStaffInformalReversed');
$settings['nameFormatStaffFormal'] = $session->get('nameFormatStaffFormal');
$settings['nameFormatStaffFormalReversed'] = $session->get('nameFormatStaffFormalReversed');
static::setup($settings);
}
/**
* Formats a YYYY-MM-DD date with the language-specific format. Optionally provide a format string to use instead.
*
* @param DateTime|string $dateString
* @param string $format
* @return string
*/
public static function date($dateString, $format = false)
{
if (empty($dateString)) {
return '';
}
$date = static::createDateTime($dateString, is_string($dateString) && strlen($dateString) == 10 ? 'Y-m-d' : null);
return $date ? $date->format($format ? $format : static::$settings['dateFormatPHP']) : $dateString;
}
/**
* Converts a date in the language-specific format to YYYY-MM-DD.
*
* @param DateTime|string $dateString
* @return string
*/
public static function dateConvert($dateString)
{
if (empty($dateString)) {
return '';
}
$date = static::createDateTime($dateString, static::$settings['dateFormatPHP']);
return $date ? $date->format('Y-m-d') : $dateString;
}
/**
* Formats a YYYY-MM-DD H:I:S MySQL timestamp as a language-specific string. Optionally provide a format string to use.
*
* @param DateTime|string $dateString
* @param string $format
* @return string
*/
public static function dateTime($dateString, $format = false)
{
if (empty($dateString)) {
return '';
}
$date = static::createDateTime($dateString, 'Y-m-d H:i:s');
return $date ? $date->format($format ? $format : static::$settings['dateTimeFormatPHP']) : $dateString;
}
/**
* Formats a YYYY-MM-DD date as a readable string with month names.
*
* @param DateTime|string $dateString The date string to format.
* @param int|string $dateFormat (Optional) An int to specify the date format used with IntlDateFormatter
* If a string is passed, it will return the default format.
* See: https://www.php.net/manual/en/class.intldateformatter.php
* See: https://unicode-org.github.io/icu/userguide/format_parse/datetime/
* Default: \IntlDateFormatter::MEDIUM
* @param int|string $timeFormat (Optional) An int to specify the time format used with IntlDateFormatter
* Default: \IntlDateFormatter::NONE
*
* @return string The formatted date string.
*/
public static function dateReadable($dateString, $dateFormat = null, $timeFormat = null) : string
{
if (empty($dateString)) {
return '';
}
if (!static::$intlFormatterAvailable) {
return static::date($dateString, static::getDateFallback($dateFormat, $timeFormat));
}
$formatter = new \IntlDateFormatter(
static::$settings['code'],
is_int($dateFormat) && $dateFormat < 100 ? $dateFormat : \IntlDateFormatter::MEDIUM,
is_int($timeFormat) ? $timeFormat : \IntlDateFormatter::NONE,
null,
null,
static::getDatePattern($dateFormat)
);
return mb_convert_case(
$formatter->format(static::createDateTime($dateString)),
MB_CASE_TITLE,
);
}
/**
* A shortcut for formatting a YYYY-MM-DD date as a readable string with month names and times.
*
* @param DateTime|string $dateString The date string to format.
* @return string The formatted date string.
*/
public static function dateTimeReadable($dateString) : string
{
return static::dateReadable($dateString, static::MEDIUM, static::SHORT);
}
/**
* Gets a IntlDateFormatter pattern string for a given format constant.
* Extends the IntlDateFormatter options by adding NO_YEAR options.
*
* @param string|int $dateFormat
* @return string The IntlDateFormatter pattern string.
*/
protected static function getDatePattern($dateFormat = null)
{
if (is_string($dateFormat)) {
return null;
}
switch ($dateFormat) {
case static::FULL_NO_YEAR:
return static::$settings['dateFormatIntlFull'];
case static::LONG_NO_YEAR:
return static::$settings['dateFormatIntlLong'];
case static::MEDIUM_NO_YEAR:
case static::SHORT_NO_YEAR:
return static::$settings['dateFormatIntlMedium'];
}
return null;
}
/**
* Gets a generic format for DateTime classes, to be used as a fallback
* when IntlDateFormatter is not available.
*
* @param string|int $dateFormat
* @param string|int $timeFormat
* @return string The DateTime format string.
*/
protected static function getDateFallback($dateFormat = null, $timeFormat = null)
{
if (is_null($dateFormat)) {
$dateFormat = static::MEDIUM;
}
switch ($dateFormat) {
case static::NONE:
$format = '';
break;
case static::FULL:
case static::FULL_NO_YEAR:
$format = static::$settings['dateFormatFull'];
break;
case static::LONG:
case static::LONG_NO_YEAR:
$format = static::$settings['dateFormatLong'];
break;
default:
$format = static::$settings['dateFormatMedium'];
}
if ($dateFormat >= 0 && $dateFormat < 100) {
$format .= ' Y';
}
if ($timeFormat != null && $timeFormat != static::NONE) {
$format .= !empty($format) ? ', ' : '';
$format .= $timeFormat == static::FULL
? 'H:i:s'
: 'H:i';
}
return $format;
}
/**
* Formats two YYYY-MM-DD dates with the language-specific format. Optionally provide a format string to use instead.
*
* @param DateTime|string $dateFrom
* @param DateTime|string $dateTo
* @return string
*/
public static function dateRange($dateFrom, $dateTo, $format = false)
{
if (empty($dateFrom) || empty($dateTo)) {
return '';
}
return static::date($dateFrom, $format) . ' - ' . static::date($dateTo, $format);
}
/**
* Formats two YYYY-MM-DD dates as a readable string, collapsing same months and same years.
*
* @param DateTime|string $dateFrom
* @param DateTime|string $dateTo
* @return string
*/
public static function dateRangeReadable($dateFrom, $dateTo)
{
$output = '';
if (empty($dateFrom) || empty($dateTo)) {
return $output;
}
$startDate = static::createDateTime($dateFrom);
$endDate = static::createDateTime($dateTo);
$startTime = $startDate->getTimestamp();
$endTime = $endDate->getTimestamp();
if ($startDate->format('Y-m-d') == $endDate->format('Y-m-d')) {
$output = static::dateReadable($startTime, static::MEDIUM);
} elseif ($startDate->format('Y') == $endDate->format('Y')) {
$output = static::dateReadable($startTime, static::MEDIUM_NO_YEAR) . ' - ';
$output .= static::dateReadable($endTime, static::MEDIUM);
} else {
$output = static::dateReadable($startTime, static::MEDIUM) . ' - ';
$output .= static::dateReadable($endTime, static::MEDIUM);
}
return mb_convert_case($output, MB_CASE_TITLE);
}
/**
* Formats a Unix timestamp as the language-specific format. Optionally provide a format string to use instead.
*
* @param DateTime|string|int $timestamp
* @param string $format
* @return string
*/
public static function dateFromTimestamp($timestamp, $format = false)
{
if (empty($timestamp)) {
return '';
}
$date = static::createDateTime($timestamp, 'U');
return $date ? $date->format($format ? $format : static::$settings['dateFormatPHP']) : $timestamp;
}
/**
* Formats a Date or DateTime string relative to the current time. Eg: 1 hr ago, 3 mins from now.
*
* @param DateTime|string $dateString
* @return string
*/
public static function relativeTime($dateString, $tooltip = true, $relativeString = true)
{
if (empty($dateString)) {
return '';
}
if (is_string($dateString) && strlen($dateString) == 10) {
$dateString .= ' 00:00:00';
}
$date = static::createDateTime($dateString, 'Y-m-d H:i:s');
$timeDifference = time() - $date->format('U');
$seconds = intval(abs($timeDifference));
switch ($seconds) {
case ($seconds <= 60):
$time = __('Less than 1 min');
break;
case ($seconds > 60 && $seconds < 3600):
$minutes = floor($seconds / 60);
$time = __n('{count} min', '{count} mins', $minutes);
break;
case ($seconds >= 3600 && $seconds < 172800):
$hours = floor($seconds / 3600);
$time = __n('{count} hr', '{count} hrs', $hours);
break;
case ($seconds >= 172800 && $seconds < 1209600):
$days = floor($seconds / 86400);
$time = __n('{count} day', '{count} days', $days);
break;
case ($seconds >= 1209600 && $seconds < 4838400):
$days = floor($seconds / 604800);
$time = __n('{count} week', '{count} weeks', $days);
break;
default:
$timeDifference = 0;
$time = static::dateReadable($dateString);
}
if ($relativeString && $timeDifference > 0) {
$time = __('{time} ago', ['time' => $time]);
} elseif ($relativeString && $timeDifference < 0) {
$time = __('in {time}', ['time' => $time]);
}
return $tooltip
? self::tooltip($time, static::dateTime($dateString))
: $time;
}
/**
* Converts a YYYY-MM-DD date to a Unix timestamp.
*
* @param DateTime|string $dateString
* @param string $timezone
* @return int
*/
public static function timestamp($dateString, $timezone = null)
{
if (empty($dateString)) {
return '';
}
if (is_string($dateString) && strlen($dateString) == 10) {
$dateString .= ' 00:00:00';
}
$date = static::createDateTime($dateString, 'Y-m-d H:i:s', $timezone);
return $date ? $date->getTimestamp() : 0;
}
/**
* Formats a time from a given MySQL time or timestamp value.
*
* @param DateTime|string $timeString
* @param string|bool $format
* @return string
*/
public static function time($timeString, $format = false)
{
if (empty($timeString)) {
return '';
}
$convertFormat = is_string($timeString) && strlen($timeString) == 8? 'H:i:s' : 'Y-m-d H:i:s';
$date = static::createDateTime($timeString, $convertFormat);
return $date ? $date->format($format ? $format : static::$settings['timeFormatPHP']) : $timeString;
}
/**
* Formats a range of times from two given MySQL time or timestamp values.
*
* @param DateTime|string $timeFrom
* @param DateTime|string $timeTo
* @param string|bool $format
* @return string
*/
public static function timeRange($timeFrom, $timeTo, $format = false)
{
return !empty($timeFrom) && !empty($timeTo)
? static::time($timeFrom, $format) . ' - ' . static::time($timeTo, $format)
: static::time($timeFrom, $format);
}
/**
* Formats a number to an optional decimal points.
*
* @param int|string $value
* @param int $decimals
* @return string
*/
public static function number($value, $decimals = 0)
{
return number_format($value, $decimals);
}
/**
* Formats a currency with a symbol and two decimals, optionally displaying the currency name in brackets.
*
* @param string|int $value
* @param bool $includeName
* @return string
*/
public static function currency($value, $includeName = false, $decimals = 2)
{
return static::$settings['currencySymbol'] . number_format($value, $decimals) . ( $includeName ? ' ('.static::$settings['currencyName'].')' : '');
}
/**
* Formats a Y/N value as Yes or No in the current language.
*
* @param string $value
* @param bool $translate
* @return string
*/
public static function yesNo($value, $translate = true)
{
$value = ($value == 'Y' || $value == 'Yes') ? 'Yes' : 'No';
return $translate ? __($value) : $value;
}
/**
* Formats a F/M/Other/Unspecified value as Female/Male/Other/Unspecified in the current language.
*
* @param string $value
* @param bool $translate
* @return string
*/
public static function genderName($value, $translate = true)
{
if (empty($value)) return '';
$genderNames = [
'F' => __('Female'),
'M' => __('Male'),
'Other' => __('Other'),
'Unspecified' => __('Unspecified')
];
return $translate ? __($genderNames[$value]) : $genderNames[$value];
}
/**
* Formats a filesize in bytes to display in KB, MB, etc.
*
* @param int $bytes
* @return string
*/
public static function filesize($bytes)
{
$unit = ['bytes','KB','MB','GB','TB','PB'];
return !empty($bytes)
? @round($bytes/pow(1024, ($i=floor(log($bytes, 1024)))), 2).' '.$unit[$i]
: '0 KB';
}
/**
* Formats a long string by truncating after $length characters
* and displaying the full string on hover.
*
* @param string $value
* @param int $length
* @return string
*/
public static function truncate($value, $length = 40)
{
return is_string($value) && strlen($value) > $length
? "".substr($value, 0, $length).'...'
: $value;
}
/**
* Formats a string of additional details in a smaller font.
*
* @param string $value
* @return string
*/
public static function small($value)
{
return ''.$value.'';
}
/**
* Formats a string in a larger font
*
* @param string $value
* @return string
*/
public static function bold($value)
{
return ''.$value.'';
}
/**
* Formats a string as a tag
*
* @param string $value
* @return string
*/
public static function tag($value, $class, $title = '')
{
return ''.$value.'';
}
/**
* Formats a string of additional details for a hover-over tooltip.
*
* @param string $value
* @return string
*/
public static function tooltip($value, $tooltip = '')
{
return ''.$value.'';
}
/**
* Formats a link from a url. Automatically adds target _blank to external links.
* Automatically resolves relative URLs starting with ./ into absolute URLs.
*
* @param string $url
* @param string $text
* @param array $attr
* @return string
*/
public static function link($url, $text = '', $attr = [])
{
if (empty($url)) {
return $text;
}
if ($text === '') {
$text = $url;
}
if (!is_array($attr)) {
$attr = ['title' => $attr];
}
if (stripos($url, '@') !== false) {
$url = 'mailto:'.$url;
}
if (substr($url, 0, 2) == './') {
$url = static::$settings['absoluteURL'].substr($url, 1);
}
if (stripos($url, static::$settings['absoluteURL']) === false && !$url instanceof Url) {
return ''.$text.'';
} else {
return ''.$text.'';
}
}
/**
* Replaces all URLs with active hyperlinks
*
* @param string $value
* @return string
*/
public static function hyperlinkAll(string $value)
{
$pattern = '/([^">]|^)(https?:\/\/[^"<>\s]+)/';
return preg_replace($pattern, '$1$2', $value);
}
/**
* Formats a key => value array of HTML attributes into a string of key="value".
*
* @param array $attributes
* @return string
*/
public static function attributes(array $attributes)
{
return implode(' ', array_map(
function ($key) use ($attributes) {
if (is_bool($attributes[$key])) {
return $attributes[$key]? $key : '';
}
if (isset($attributes[$key]) && $attributes[$key] != '') {
return $key.'="'.htmlentities($attributes[$key], ENT_QUOTES, 'UTF-8').'"';
}
return '';
},
array_keys($attributes)
));
}
/**
* Formats a YYYY-MM-DD date as a relative age with years and months.
*
* @param string $dateString
* @param bool $short
* @return string
*/
public static function age($dateString, $short = false)
{
if (empty($dateString)) {
return '';
}
$date = DateTime::createFromFormat('Y-m-d', $dateString);
if (!$date) {
return __('Unknown');
}
$date = $date->diff(new DateTime());
return $short
? $date->y . __('y') .', '. $date->m . __('m')
: $date->y .' '. __('years') .', '. $date->m .' '. __('months');
}
/**
* Formats phone numbers, optionally including countrt code and types.
* Adds spaces to 7-10 digit numbers based on the most common global formats.
*
* @param string|int $number
* @param bool $countryCode
* @param bool $type
* @return string
*/
public static function phone($number, $countryCode = false, $type = false)
{
$number = preg_replace('/[^0-9]/', '', $number);
switch (strlen($number)) {
case 7:
$number = preg_replace('/([0-9]{3})([0-9]{4})/', '$1 $2', $number);
break;
case 8:
$number = preg_replace('/([0-9]{4})([0-9]{4})/', '$1 $2', $number);
break;
case 9:
$number = preg_replace('/([0-9]{3})([0-9]{2})([0-9]{2})([0-9]{2})/', '$1 - $2 $3 $4', $number);
break;
case 10:
$number = preg_replace('/([0-9]{3})([0-9]{3})([0-9]{4})/', '($1) $2 $3', $number);
break;
}
return ($type? $type.': ' : '') . ($countryCode? '+'.$countryCode.' ' : '') . $number;
}
/**
* Formats an address including optional district and country.
*
* @param string $address
* @param string $addressDistrict
* @param string $addressCountry
* @return string
*/
public static function address($address, $addressDistrict, $addressCountry)
{
if (stripos($address, PHP_EOL) === false) {
// If the address has no line breaks, collapse lines by comma separation,
// breaking up long address lines over 30 characters.
$collapseAddress = function ($list, $line = '') use (&$collapseAddress) {
$line .= array_shift($list);
if (empty($list)) {
return $line;
}
return strlen($line.', '.current($list)) > 30
? $line.'
'.$collapseAddress($list, '')
: $collapseAddress($list, $line.', ');
};
$addressLines = array_filter(array_map('trim', explode(',', $address)));
$address = $collapseAddress($addressLines);
} else {
$address = nl2br($address);
}
return ($address? $address.'
' : '') . ($addressDistrict? $addressDistrict.'
' : '') . ($addressCountry? $addressCountry.'
' : '');
}
public static function list(array $items, $tag = 'ul', $listClass = '', $itemClass = 'leading-normal')
{
$output = "<$tag class='$listClass'>";
foreach ($items as $item) {
$output .= "
".$label.' | '; } $output .= ""; foreach ($items as $index => $item) { $output .= "
---|
".($item[$key] ?? '').' | '; } $output .= "