Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve date & time formats #8460

Merged
merged 15 commits into from
Aug 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This is a changelog for Piwik platform developers. All changes for our HTTP API's, Plugins, Themes, etc will be listed here.

## Piwik 3.0.0

### Breaking Changes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sgiehl Can you also note in CHANGELOG that prettyDate attribute on API response has slightly changed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

* Handling of localized date, time and range formats has been changed. Patterns no longer contain placeholders like `%shortDay%`, but work with CLDR pattern instead. You can use one of the predefined format constants in `Date` class for using `getLocalized()`.

## Piwik 2.14.0

### Breaking Changes
Expand Down
236 changes: 216 additions & 20 deletions core/Date.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*
* $date = Date::factory('2007-07-24 14:04:24', 'EST');
* $date->addHour(5);
* echo $date->getLocalized("%longDay% the %day% of %longMonth% at %time%");
* echo $date->getLocalized("EEE, d. MMM y 'at' HH:mm:ss");
*
* @api
*/
Expand All @@ -41,6 +41,16 @@ class Date
/** The default date time string format. */
const DATE_TIME_FORMAT = 'Y-m-d H:i:s';

const DATETIME_FORMAT_LONG = 'Intl_Format_DateTime_Long';
const DATETIME_FORMAT_SHORT = 'Intl_Format_DateTime_Short';
const DATE_FORMAT_LONG = 'Intl_Format_Date_Long';
const DATE_FORMAT_DAY_MONTH = 'Intl_Format_Date_Day_Month';
const DATE_FORMAT_SHORT = 'Intl_Format_Date_Short';
const DATE_FORMAT_MONTH_SHORT = 'Intl_Format_Month_Short';
const DATE_FORMAT_MONTH_LONG = 'Intl_Format_Month_Long';
const DATE_FORMAT_YEAR = 'Intl_Format_Year';
const TIME_FORMAT = 'Intl_Format_Time';

/**
* Max days for months (non-leap-year). See {@link addPeriod()} implementation.
*
Expand Down Expand Up @@ -605,7 +615,38 @@ public function subYear($n)
* Returns a localized date string using the given template.
* The template should contain tags that will be replaced with localized date strings.
*
* Allowed tags include:
* @param string $template eg. `"MMM y"`
* @return string eg. `"Aug 2009"`
*/
public function getLocalized($template)
{
$template = $this->replaceLegacyPlaceholders($template);

if (substr($template, 0, 5) == 'Intl_') {
$translator = StaticContainer::get('Piwik\Translation\Translator');
$template = $translator->translate($template);
}

$tokens = self::parseFormat($template);

$out = '';

foreach ($tokens AS $token) {
if (is_array($token)) {
$out .= $this->formatToken(array_shift($token));

} else {
$out .= $token;
}
}

return $out;
}

/**
* Replaces legacy placeholders
*
* @deprecated should be removed in Piwik 3.0.0 or later
*
* - **%day%**: replaced with the day of the month without leading zeros, eg, **1** or **20**.
* - **%shortMonth%**: the short month in the current language, eg, **Jan**, **Feb**.
Expand All @@ -615,28 +656,183 @@ public function subYear($n)
* - **%longYear%**: the four digit year, eg, **2007**, **2013**.
* - **%shortYear%**: the two digit year, eg, **07**, **13**.
* - **%time%**: the time of day, eg, **07:35:00**, or **15:45:00**.
*
* @param string $template eg. `"%shortMonth% %longYear%"`
* @return string eg. `"Aug 2009"`
*/
public function getLocalized($template)
protected function replaceLegacyPlaceholders($template)
{
if (strpos($template, '%') === false) {
return $template;
}

$mapping = array(
'%day%' => 'd',
'%shortMonth%' => 'MMM',
'%longMonth%' => 'MMMM',
'%shortDay%' => 'EEE',
'%longDay%' => 'EEEE',
'%longYear%' => 'y',
'%shortYear%' => 'yy',
'%time%' => 'HH:mm:ss'
);

return str_replace(array_keys($mapping), array_values($mapping), $template);
}

protected function formatToken($token)
{
$translator = StaticContainer::get('Piwik\Translation\Translator');
$day = $this->toString('j');
$dayOfWeek = $this->toString('N');
$monthOfYear = $this->toString('n');
$patternToValue = array(
"%day%" => $day,
"%shortMonth%" => $translator->translate('Intl_ShortMonth_' . $monthOfYear),
"%longMonth%" => $translator->translate('Intl_LongMonth_' . $monthOfYear),
"%shortDay%" => $translator->translate('Intl_ShortDay_' . $dayOfWeek),
"%longDay%" => $translator->translate('Intl_LongDay_' . $dayOfWeek),
"%longYear%" => $this->toString('Y'),
"%shortYear%" => $this->toString('y'),
"%time%" => $this->toString('H:i:s')
);
$out = str_replace(array_keys($patternToValue), array_values($patternToValue), $template);
return $out;
$translator = StaticContainer::get('Piwik\Translation\Translator');

switch ($token) {
// year
case "yyyy":
case "y":
return $this->toString('Y');
case "yy":
return $this->toString('y');
// month
case "MMMM":
return $translator->translate('Intl_Month_Long_' . $monthOfYear);
case "MMM":
return $translator->translate('Intl_Month_Short_' . $monthOfYear);
case "MM":
return $this->toString('n');
case "M":
return $this->toString('m');
case "LLLL":
return $translator->translate('Intl_Month_Long_StandAlone_' . $monthOfYear);
case "LLL":
return $translator->translate('Intl_Month_Short_StandAlone_' . $monthOfYear);
case "LL":
return $this->toString('n');
case "L":
return $this->toString('m');
// day
case "dd":
return $this->toString('d');
case "d":
return $this->toString('j');
case "EEEE":
return $translator->translate('Intl_Day_Long_' . $dayOfWeek);
case "EEE":
case "EE":
case "E":
return $translator->translate('Intl_Day_Short_' . $dayOfWeek);
case "CCCC":
return $translator->translate('Intl_Day_Long_StandAlone_' . $dayOfWeek);
case "CCC":
case "CC":
case "C":
return $translator->translate('Intl_Day_Short_StandAlone_' . $dayOfWeek);
case "D":
return 1 + (int)$this->toString('z'); // 1 - 366
case "F":
return (int)(((int)$this->toString('j') + 6) / 7);
// week in month
case "w":
$weekDay = date('N', mktime(0, 0, 0, $this->toString('m'), 1, $this->toString('y')));
return floor(($weekDay + (int)$this->toString('m') - 2) / 7) + 1;
// week in year
case "W":
return $this->toString('N');
// hour
case "HH":
return $this->toString('H');
case "H":
return $this->toString('G');
case "hh":
return $this->toString('h');
case "h":
return $this->toString('g');
// minute
case "mm":
case "m":
return $this->toString('i');
// second
case "ss":
case "s":
return $this->toString('s');
// am / pm
case "a":
return $this->toString('a') == 'am' ? $translator->translate('Intl_Time_AM') : $translator->translate('Intl_Time_PM');

// currently not implemented:
case "G":
case "GG":
case "GGG":
case "GGGG":
case "GGGGG":
return ''; // era
case "z":
case "Z":
case "v":
return ''; // time zone

}

return '';
}

protected static $tokens = array(
'G', 'y', 'M', 'L', 'd', 'h', 'H', 'm', 's', 'E', 'c', 'e', 'D', 'F', 'w', 'W', 'a', 'z', 'Z', 'v',
);

/**
* Parses the datetime format pattern and returns a tokenized result array
*
* Examples:
* Input Output
* 'dd.mm.yyyy' array(array('dd'), '.', array('mm'), '.', array('yyyy'))
* 'y?M?d?EEEE ah:mm:ss' array(array('y'), '?', array('M'), '?', array('d'), '?', array('EEEE'), ' ', array('a'), array('h'), ':', array('mm'), ':', array('ss'))
*
* @param string $pattern the pattern to be parsed
* @return array tokenized parsing result
*/
protected static function parseFormat($pattern)
{
static $formats = array(); // cache
if (isset($formats[$pattern])) {
return $formats[$pattern];
}
$tokens = array();
$n = strlen($pattern);
$isLiteral = false;
$literal = '';
for ($i = 0; $i < $n; ++$i) {
$c = $pattern[$i];
if ($c === "'") {
if ($i < $n - 1 && $pattern[$i + 1] === "'") {
$tokens[] = "'";
$i++;
} elseif ($isLiteral) {
$tokens[] = $literal;
$literal = '';
$isLiteral = false;
} else {
$isLiteral = true;
$literal = '';
}
} elseif ($isLiteral) {
$literal .= $c;
} else {
for ($j = $i + 1; $j < $n; ++$j) {
if ($pattern[$j] !== $c) {
break;
}
}
$p = str_repeat($c, $j - $i);
if (in_array($c, self::$tokens)) {
$tokens[] = array($p);
} else {
$tokens[] = $p;
}
$i = $j - 1;
}
}
if ($literal !== '') {
$tokens[] = $literal;
}
return $formats[$pattern] = $tokens;
}

/**
Expand Down
63 changes: 63 additions & 0 deletions core/Period.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,67 @@ public function getRangeString()

return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d");
}


/**
* @param string $format
*
* @return mixed
*/
protected function getTranslatedRange($format)
{
$dateStart = $this->getDateStart();
$dateEnd = $this->getDateEnd();
list($formatStart, $formatEnd) = $this->explodeFormat($format);

$string = $dateStart->getLocalized($formatStart);
$string .= $dateEnd->getLocalized($formatEnd);

return $string;
}

/**
* Explodes the given format into two pieces. One that can be user for start date and the other for end date
*
* @param $format
* @return array
*/
protected function explodeFormat($format)
{
$intervalTokens = array(
array('d', 'E', 'C'),
array('M', 'L'),
array('y')
);

$offset = strlen($format);

// search for first duplicate date field
foreach ($intervalTokens AS $tokens) {
if (preg_match_all('/[' . implode('|', $tokens) . ']+/', $format, $matches, PREG_OFFSET_CAPTURE) &&
count($matches[0]) > 1 && $offset > $matches[0][1][1]
) {
$offset = $matches[0][1][1];
}
}

return array(substr($format, 0, $offset), substr($format, $offset));
}

protected function getRangeFormat($short = false)
{
$maxDifference = 'D';
if ($this->getDateStart()->toString('y') != $this->getDateEnd()->toString('y')) {
$maxDifference = 'Y';
} elseif ($this->getDateStart()->toString('m') != $this->getDateEnd()->toString('m')) {
$maxDifference = 'M';
}

return $this->translator->translate(
sprintf(
'Intl_Format_Interval_%s_%s',
$short ? 'Short' : 'Long',
$maxDifference
));
}
}
9 changes: 3 additions & 6 deletions core/Period/Day.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Piwik\Period;

use Exception;
use Piwik\Date;
use Piwik\Period;
use Piwik\Piwik;

Expand Down Expand Up @@ -38,9 +39,7 @@ public function getLocalizedShortString()
{
//"Mon 15 Aug"
$date = $this->getDateStart();
$template = $this->translator->translate('CoreHome_ShortDateFormat');

$out = $date->getLocalized($template);
$out = $date->getLocalized(Date::DATE_FORMAT_DAY_MONTH);
return $out;
}

Expand All @@ -53,9 +52,7 @@ public function getLocalizedLongString()
{
//"Mon 15 Aug"
$date = $this->getDateStart();
$template = $this->translator->translate('CoreHome_DateFormat');

$out = $date->getLocalized($template);
$out = $date->getLocalized(Date::DATE_FORMAT_LONG);
return $out;
}

Expand Down
4 changes: 2 additions & 2 deletions core/Period/Month.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Month extends Period
public function getLocalizedShortString()
{
//"Aug 09"
$out = $this->getDateStart()->getLocalized($this->translator->translate('CoreHome_ShortMonthFormat'));
$out = $this->getDateStart()->getLocalized(Date::DATE_FORMAT_MONTH_SHORT);
return $out;
}

Expand All @@ -37,7 +37,7 @@ public function getLocalizedShortString()
public function getLocalizedLongString()
{
//"August 2009"
$out = $this->getDateStart()->getLocalized($this->translator->translate('CoreHome_LongMonthFormat'));
$out = $this->getDateStart()->getLocalized(Date::DATE_FORMAT_MONTH_LONG);
return $out;
}

Expand Down
Loading