Problem/Motivation

Currently, the only function that exists in Drupal Core to format date intervals is DateFormatter::formatInterval(). The input to this function is a number of seconds.

This is fine for small intervals, up to a couple of weeks. However, if the interval goes over 30 days, it is not possible to say how many "months" is really being represented, even though the function will return a formatted string containing "months" and "years" -- so it is not accurate.

The reason is that a "month" is not a fixed number of seconds -- for example, it's 30 days between April 10 and May 10, but 31 between May 10 and June 10, but both of those should be formatted as "1 month". Similarly, a "year" is not a fixed number of seconds, because some years are 365 days and some are 366.

Another problem is that the formatInterval() method currently defines a "month" as 30 days and a year as 365 days, which means that there are not 12 months exactly in a year. This leads to "interesting" sequences, such as #2430529: "Member for" date format has a funny way of describing 11 years, and the strange jumps between 11 months, 12 months, and 1 year that you can see around this page in Tracker: https://www.drupal.org/user/124982/track?page=101 (if you don't see it there, try going to a page or two older until you get to the 1 year mark).

Proposed resolution

The only way to make an accurate formatting of a time interval that is longer than a month or so is to know what the start and end date/times are for the interval, so that you can tell whether, for instance, a given number of seconds is the difference between April 10 and May 11 (1 month 1 day) or May 10 and May 11 (1 month) -- these are the same number of seconds, but the output should be different.

So the proposed fix is to make a new function that has inputs for the start and end as date/times, rather than a single input of the number of seconds in the difference between them. Specifically, the proposal is to introduce DateFormatter::formatDiff(), which should be used when the start and end date of the interval are known. The function can use the DateTime and DateInterval class provided by PHP to do the work of formatting. We'll also provide thin wrappers to do differences to "now", in the past and future.

Most uses in Core of formatInterval() will also be converted to use these new methods. However, a few will remain as legitimate uses of formatInterval() because they don't have two times to compare:

a) Various form builders (and their tests) - used to create option lists for intervals for cron and things like that, example:
core/lib/Drupal/Core/Block/BlockBase.php:

   $period = array_map(array(\Drupal::service('date.formatter'), 'formatInterval'), array_combine($period, $period));

These are actually intervals, so we cannot convert them to use formatDiff or the wrappers.

b) core/modules/views/src/Plugin/views/field/TimeInterval.php -- this is a Views field handler for a field whose value is a time interval in seconds. As it has no start/end dates, it needs to still use formatInterval.

c) Batch UI - formats the time elapsed for the request. This is OK to stay as a formatInterval and would require a larger rewrite of the Batch UI to change it anyway (unlikely to be longer than a few minutes in any case, so formatInterval is accurate there).

Note: There are also discussions on #445414: DateFormatter::formatInterval() doesn't format monthly intervals correctly about whether formatInterval() itself needs to be improved to fix the "12 months is not a year" problem.

Remaining tasks

- Add the new base function.
- Add unit test coverage for the new function.
- Replace all occurrences in core where the interval is the difference between two dates, to use the new function.
- Leave occurrences in core where the interval is not a difference between two dates alone
- Replace instances with the thin wrappers when the second date is implicitly the current Request's REQUEST_TIME.
- Expand test coverage to cover the thin wrappers.
- If needed, add web test coverage for the changes in core. However, existing uses of formatItnterval() already have tests; they do not cover cases where the date intervals are greater than 30 days, but since we will have unit test coverage for the new function, that should be OK.
- Review - needs to verify that all remaining uses of formatInterval are legitimate (see Proposed Resolution), and that the conversions from formatInterval to the new functions are correct.
- Commit
- Publish the CR

User interface changes

Formatted "x time ago" and "x time hence" dates should be more accurate. They are currently inaccurate if they are more than 30 days ago/hence.

API changes

No changes to existing API.

API additions: A new function DateFormatter::formatDiff() is introduced, which can be used to calculate and format a datetime interval correctly. Two thin wrappers, DateFormatter::formatTimeSince() and DateFormatter::formatTimeUntil(), are also added when the second date is implicitly the time of the current request.

A change record can be found here: https://www.drupal.org/node/2497341

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because date formatting is not accurate as-is for "time ago" formatting more than a month in the past.
Issue priority Normal: the current implementation is wrong since it doesn't take a start and end date into account, it sometimes returns unexpected results, and core uses this in several places. Not really major though since it only affects time intervals greater than a month, which is a lot of them (when it's used for comment times etc.) but it's not that crucial to get them exactly right and there aren't many cases where they're really pathological.
Prioritized changes Prioritized: The main goal of this issue is fixing a bug in the formatting of date intervals when a start and end date are known and the interval is greater than a month.
Disruption None. New API functions are added. Old one remains. New ones are used in Core where appropriate; old one is still needed for a few cases so it is retained.
CommentFileSizeAuthor
#174 2456521-d7-171-174-interdiff.txt9.18 KBSharique
#174 add-2456521-174.patch18.39 KBSharique
#172 add-2456521-172.patch19.82 KBSharique
#171 2456521-d7-171.patch8.25 KBvijaycs85
#164 add-2456521-164.patch50.6 KBmpdonadio
#157 add-2456521-156.patch50.61 KBAnonymous (not verified)
#157 interdiff-154-156.txt1.14 KBAnonymous (not verified)
#154 add-2456521-154.patch50.61 KBAnonymous (not verified)
#154 inter-147-154.txt2.39 KBAnonymous (not verified)
#147 add-2456521-146.patch49.86 KBmpdonadio
#142 add-2456521-142.patch49.26 KBAnonymous (not verified)
#142 inter-138-142.txt2.34 KBAnonymous (not verified)
#138 add-2456521-138.patch48.58 KBAnonymous (not verified)
#138 inter-137-138.txt5.98 KBAnonymous (not verified)
#134 add-2456521-134.patch48.43 KBAnonymous (not verified)
#134 interdiff-133-134.txt26.76 KBAnonymous (not verified)
#133 add-2456521-132.patch46.14 KBAnonymous (not verified)
#133 interdiff-130-132.txt20.62 KBAnonymous (not verified)
#130 add-2456521-130.patch45.67 KBAnonymous (not verified)
#130 interdiff-124-130.txt4.67 KBAnonymous (not verified)
#124 add-formatDiff-2456521-124.patch45.78 KBAnonymous (not verified)
#124 inter-121-124.txt16.93 KBAnonymous (not verified)
#121 interdiff-110-121.patch4.89 KBmpdonadio
#121 add-2456521-121.patch42.99 KBmpdonadio
#110 interdiff-108-110.txt1.27 KBmpdonadio
#110 2456521-date-diff-formatter-110.patch39.34 KBmpdonadio
#108 2456521-date-diff-formatter-108.patch39.32 KBmpdonadio
#105 interdiff.txt12.61 KBjhodgdon
#105 2456521-date-diff-formatter-105.patch39.3 KBjhodgdon
#103 interdiff.txt887 bytesjhodgdon
#103 2456521-date-diff-formatter-103.patch39.71 KBjhodgdon
#101 interdiff.txt13.11 KBjhodgdon
#101 2456521-date-diff-formatter-101.patch39.72 KBjhodgdon
#82 add-2456521-82.patch26.45 KBAnonymous (not verified)
#82 interdiff-79-82.txt5.07 KBAnonymous (not verified)
#80 interdiff-78-79.txt1005 bytesAnonymous (not verified)
#79 add-2456521-79.patch27.24 KBAnonymous (not verified)
#79 add-2456521-79.patch27.24 KBAnonymous (not verified)
#78 add-2456521-78.patch27.23 KBAnonymous (not verified)
#78 interdiff-76-78.txt6.68 KBAnonymous (not verified)
#76 add-2456521-76.patch26.97 KBAnonymous (not verified)
#76 interdiff-74-76.txt800 bytesAnonymous (not verified)
#74 add-2456521-74.patch27 KBAnonymous (not verified)
#74 interdiff-72-74.txt946 bytesAnonymous (not verified)
#72 add-2456521-72.patch27.01 KBAnonymous (not verified)
#72 interdiff-63-72.txt12.32 KBAnonymous (not verified)
#63 2456521-63.patch25.23 KBAnonymous (not verified)
#63 interdiff-60-63.txt13.74 KBAnonymous (not verified)
#60 2456521-60.patch25.95 KBAnonymous (not verified)
#60 2456521-60.patch25.95 KBAnonymous (not verified)
#59 2456521-59.patch25.95 KBAnonymous (not verified)
#59 interdiff-52-59.txt12.86 KBAnonymous (not verified)
#52 2456521-52.patch26.63 KBAnonymous (not verified)
#52 interdiff-50-52.txt583 bytesAnonymous (not verified)
#50 2456521-50.patch26.67 KBAnonymous (not verified)
#50 interdiff-47-50.txt1.23 KBAnonymous (not verified)
#47 2456521-47.patch26.39 KBAnonymous (not verified)
#47 interdiff-46-47.txt1.2 KBAnonymous (not verified)
#46 2456521_46.patch26.23 KBAnonymous (not verified)
#46 interdiff-42-46.txt3.51 KBAnonymous (not verified)
#42 2456521-42.patch25.21 KBAnonymous (not verified)
#42 interdiff-40-42.txt24 KBAnonymous (not verified)
#40 2456521-40.patch14.14 KBAnonymous (not verified)
#40 interdiff-33-40.txt9.87 KBAnonymous (not verified)
#33 interdiff.txt10.2 KBrteijeiro
#33 2456521-33.patch13.58 KBrteijeiro
#26 2456521-26.patch13.48 KBAnonymous (not verified)
#26 interdiff-17-26.txt12.14 KBAnonymous (not verified)
#17 2456521-17.patch6.12 KBAnonymous (not verified)
#10 dateintervals-2456521-10.patch5.62 KBAnonymous (not verified)
#9 interdiff.txt1.05 KBrteijeiro
#9 format-interval-clarify-docs-2456521-9.patch1.22 KBrteijeiro
#2 format-interval-clarify-docs.patch1.22 KBjhodgdon
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jhodgdon’s picture

The best option in my opinion would be to remove the month/year units from this class. The rest is OK. The month/year stuff is simply wrong.

Then add a method that could be used to format a time difference the right way, with two date/time inputs.

jhodgdon’s picture

Status: Active » Needs review
FileSize
1.22 KB

Let's at least fix the docs so they warn people about this problem.

jhodgdon’s picture

I also don't think we should be using this in Core to format intervals between two dates, unless we know they're under 30 day intervals. We should write a better function that does it right.

yched’s picture

This code is a direct transposition of format_interval(), which dates back to at least 2001. e2f051b seems to be the commit that settled its logic as we have it atm, from the dates it seems that's before 3.0.0 was tagged (git tags don't seem to go further up)

The logic does look a bit sloppy, but not sure 14-year-old behavior can really be qualified as major ? ;-)

jhodgdon’s picture

It's OK with me if you want to demote the issue, I'm not going to get into a Priority war over it. But just because some logic has been around for 14 years doesn't mean it isn't flawed, and isn't being misused.

And this method really shouldn't be used the way it's being used all over Core. The idea of making "ago" formatting based on fixed number of seconds is only valid up to 30 days, so if an interval could be more than that, it doesn't work well. I'm not too worried about it on the "How long is it since you ran cron" output, which hopefully shouldn't be all that long.

But this function is also being suggested for use in a generic formatter for date/time fields, where the input could be anything, and we don't know what the output is being used for, the output could be quite misleading and/or incorrect. See #2399211: Support all options from views fields in DateTime formatters. And there are quite a few other uses in core already.

larowlan’s picture

14 years ago php was a different beast (hi to all those using php that long, ah the memories) we have better tools at our disposal now in the interval and diff date time objects. +1 to removing year/month logic into method that takes two date time objects.

mpdonadio’s picture

Is there an existing implementation to use as reference, or a time library that we could use? Resig's stops at weeks, and jQuery Timeago plugin is a little more obvious when getting into the month/year range about being fuzzy.

jhodgdon’s picture

Correct formatting is built into PHP's DateInterval class, which you can generate by taking the difference of two DateTime objects:
http://php.net/manual/en/dateinterval.format.php
See example #3 for instance, and the other documentation on the class.

rteijeiro’s picture

Fixed comments that fit in 80 chars lines.

Anonymous’s picture

Issue tags: +Needs tests
FileSize
5.62 KB

Let's add a patch to get the ball rolling.

Some remarks:
- DateTimePlus::diff(): I know several ways how to do this in java, but I couldn't find a php equivalent for them. There is probably a better alternative than taking a deep copy.
- DateIntervalPlus variables: I overruled them all to determine the correct order while iterating. The $w parameter, which holds weeks, is the only extension on \DateInterval.
- The test fails: well, those prove the point of this issue. Additional coverage would be needed though.
- Let's not forget to fix the documentation for the changes we make.

I know this would still need a decent amount of work, but I didn't want to spend too much time on an implementation without hearing some honest opinions first.

Status: Needs review » Needs work

The last submitted patch, 10: dateintervals-2456521-10.patch, failed testing.

jhodgdon’s picture

Ummm... I attempted to review this patch, but ... I'm could not understand the code at all. Can you give some explanation, or better yet put in some code comments and doc headers to explain what it's doing? I have no idea.

But fundamentally: why would we not just use the built-in PHP classes/code to create and format intervals? It looks like maybe the only thing is that it doesn't format using weeks. Are they that important that we need them? I doubt it, especially if it means sacrificing accuracy.

Anonymous’s picture

Yes, the weeks are the problem here. I didn't feel very satisfied with #11 either, because I felt like it was quite complex for what we are trying to achieve. That's why I posted it before continuing.

What the patch does is:
1. Create two DateTime objects (today and today - $interval)
2. Create a DateTimePlus::diffWithWeeks() of those. This is the DateTime::diff() but adds support for weeks (and I just noticed my weeks apparently only have 5 days :/)
3. The DateFormatterPlus::diffWithWeeks() returns a DateIntervalPlus, which is a PHP DateInterval with a week parameter added.
4. In DateFormatter::formatInterval(), we loop over the DateIntervalPlus and create the output string.

An alternative approach would be to add all "week support" in the DateFormatter::formatInterval() method.

If we can drop the week support however, the patch would be greatly simplified. We would only need 1. and 4. The changes in DateTimePlus and DateIntervalPlus would become irrelevant. I'm leaning towards this option as well.

jhodgdon’s picture

OK. Here is what I think we should do:

a) Use the patch in #9 to warn people not to use formatInterval for cases where it is inappropriate. Don't change the code in this method... although I think that the code in formatInterval() is not properly translated??!? So we should fix that!

b) Make a new method DateFormatter::formatDiff that takes as input:
- Two timestamps, with the second one defaulting to the current time
- Time zone
- Granularity
- Language code

This method would:
- Convert both timestamps to DateTime objects using the DateTime::setTimestamp() method, setting time zone as well. http://php.net/manual/en/datetime.settimestamp.php and http://php.net/manual/en/datetime.settimezone.php
- Find the difference using DateTime::diff(). This will return a DateInterval object. http://php.net/manual/en/datetime.diff.php
- Format this using DateInterval::format() with a special format string of '%R.%y.%m.%d.%h.%i.%s'. http://php.net/manual/en/dateinterval.format.php
- Use explode() to separate this into pieces, and with the granularity, figure out which pieces to print and what the values are. For instance, the result might be that it's negative (from the %R), and with granularity 2 we need to format it as 2 years and 5 days.
- Use format_plural() appropriately to format each piece, and concatenate them together. So we might have a call to format_plural(2, '1 year', '%count years'). Each piece needs to have its own call to format_plural() with the strings ***in the call*** or it will not be translated properly.
- Finally use t() to make this "ago" or ... not sure what we use for "in the future"?

c) Make sure this new method and formatInterval() link to each other with @see

d) Look at the uses of formatInterval() in Core at the moment and change most/all of them to use this new method.

Does this make sense? If so, do you want to make a patch or should I?

Anonymous’s picture

Assigned: Unassigned »

Seems legit. I'll start on a patch tonight.

What is the reason to use DateInterval::format() / explode()?
Do we address a) and d) in a single patch or should we do that in follow-ups?

jhodgdon’s picture

The reason I suggested using DateInterval::format() is that it leaves the calculation of everything to DateInterval... However, I just realized (duh!) that DateInterval has public properties that give you $y, $m, etc. so that is not necessary. ;)

I think we should probably do everything in this patch, otherwise it will not be clear why this method is even needed.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
6.12 KB

Posting a work in progress. I'm curious how many tests I already broke.

Re #14:
a) I think this->formatPlural() takes care of the translation.
b)
- What about a parameter "absolute" that indicates whether or not to prepend a minus sign?
- The 'ago', 'hence' part is not added by this method. See for example \Drupal\views\Plugin\views\field\Date::render()
c) ok
d) only changed some for now, most of them are still todo

Status: Needs review » Needs work

The last submitted patch, 17: 2456521-17.patch, failed testing.

jhodgdon’s picture

Regarding #14(a) translation... Normally when you call t() or format_plural(), or equivalent methods, you need to pass in the literal strings and not variables.

It turns out that in this case, the Drupal-specific POTX extraction code special-cases the hard-coded strings in the formatInterval class (or format_interval function in previous versions), because whoever wrote the function didn't bother to call format_plural() correctly.

This should be fixed. I discussed it with Gabor in IRC and we agreed on that; and our new method should do the same. Basically you need to have a switch/case statement instead of putting the strings on a $units member variable.

Regarding +/- or ago/hence... maybe the new method could have an input that indicates how to handle this? There could be a few options, like:
- Use - on each element to indicate past, so your output would be like "-2 years - 3 days" vs. "2 years 3 days"
- Use - in the front once, so it would be something like "- 2 years 3 days" vs. "2 years 3 days"
- Same as those two options, but use +/- so it would be "-2 years - 3 days" vs. "+2 years +3 days"
- Use ago/hence, so it would be something like "2 years 3 days ago" vs. "2 years 3 days hence"

? just brainstorming here. We are creating a new method, so we can make it do whatever we want. What are the uses in Core, do they always use ago/hence? If so that should be the default?

mpdonadio’s picture

Are these changes going to be added to \Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampAgoFormatter, too?

In working on #2455125: Update EntityViewsData use of generic timestamp to use Field API formatter, I may run into some views that are using timeago. If the timeago is being touched here, it makes sense to mirror the changes in TimestampAgoFormatter, too.

jhodgdon’s picture

These are changes to the underlying API, not to any formatter. But inasmuch as the TimestampAgoFormatter calls formatInterval, yes we would find all places where Core is calling this and replace them probably with calls to this new method.

Anonymous’s picture

@jhodgdon:
- I think I understand what you are saying and how the switch/case needs to be implemented, but I don't get how that is possible. Could you point me to the 'Drupal-specific POTX extraction code'? I think I might learn a thing or two there :)

- Working from your examples, I think we could add $prefix (string), $repeat_prefix (boolean) and $suffix (string) to the parameters to cover every use case. We would have 8 arguments , of which 1 would be required.
Since I see '2 years 3 days' as a whole, options 2 and 4 'feel' best for me. That's why I wouldn't add the $repeat_prefix variable. But maybe some languages default to something I'm not used to, I'm not sure.

In core we call this function 19 times (ago, hence, left or nothing (raw)). I'd make the default an empty string since I think it is used like that the most. Ago comes in second.

@mpdonadio:
For TimestampAgoFormatter that would mean one method call would be replaced (line 90)

jhodgdon’s picture

So right now, formatInterval is doing:

  foreach ($this->units as $key => $value) {
    $key = explode('|', $key);
    if ($interval >= $value) {
      $output .= ($output ? ' ' : '') . $this->formatPlural(floor($interval / $value), $key [0], $key [1], array(), array('langcode' => $langcode));

What it needs to be doing is something like this for $units:

protected $units = array(
  'years' => 31536000,
  'months' => 2592000,
...
);

and then in that $output line, something like:

  switch ($key) {
     case 'years':
        $this_one = format_plural(floor($interval / $value), '1 year', '@count years', $langcode);
...

And here is the code that hard-wires these strings in the "potx" project:
http://cgit.drupalcode.org/potx/tree/potx.inc#n1495
If t() and format_plural() are called properly, with literal strings instead of variables, kludges like this are NOT necessary.

I like the idea of the arguments, and am fine with getting rid of the ability to repeat the - sign. We can always add it later if we need it.

jhodgdon’s picture

Oh and maybe the args could be consolidated, so the prefix/suffix are an array of two values, for what to do if it's positive and what to do if it's negative? What about if the interval is zero too? ?!?

So it could be something like callThis($date1, $date2, array('hence', 'ago')) of course that needs t() in there... but you get the idea.

Anonymous’s picture

kludges like this
Wow.

How about changing the prefix/suffix attributes it to an $attributes array instead?

$attributes(
  'prefix' => '',
  'suffix' => 'ago',
);

That way we can extend it more easily later on. Maybe we could also add the optional $timezone, $granularity and $langcode there as well?

Anonymous’s picture

Status: Needs work » Needs review
FileSize
12.14 KB
13.48 KB

In the mean time I made some progress:
- I added the switch as you suggested.
- I removed the $interval_units variable, since I don't see why we need it. The magic strings will be there anyhow.
- I started working on a unit test. I intend to expand it by adding the prefix/suffix/granularity options at random. I will however need to add some tests with a different $timestamp2, $timezone and $language parameter.

Anonymous’s picture

Locally fixed the extra spaces before testFormatInterval(), so no need to highlight that :)

jhodgdon’s picture

Status: Needs review » Needs work

Attributes: good idea... although perhaps calling it $options would be better, since $attributes tends to be used in code specifically for HTML element attributes? Also putting them all in one array makes it easy to use array addition to set defaults. Also it makes it a LOT easier for someone to set one value and ignore the rest.

Most recent patch is looking good -- coming together! A few thoughts:

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,79 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   prepended with a minus sign if timestamp1 is larger than timestamp2.
    ...
    +    $date_time1->setTimezone(new \DateTimeZone($timezone));
    +
    +    $date_time2 = new \DateTime();
    +    $date_time2->setTimestamp($timestamp2);
    +    $date_time2->setTimezone(new \DateTimeZone($timezone));
    +
    

    I would avoid the word "larger" here, talking about times, and instead use either "before" or "after".

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,79 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +    $date_time1->setTimezone(new \DateTimeZone($timezone));
    +
    +    $date_time2 = new \DateTime();
    +    $date_time2->setTimestamp($timestamp2);
    +    $date_time2->setTimezone(new \DateTimeZone($timezone));
    +
    

    Slight optimization: could just create the TimeZone object once?

  3. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,79 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +    foreach ($interval as $key => $value) {
    +      if ($value > 0) {
    +        // Switch over the keys to call formatPlural() explicitly with literal
    

    Hm. This makes me a bit uncomfortable. $interval is a date-time-interval class object, not an array. Are we sure what order this comes in as, and that there are not any values in the foreach besides the ones you think should be there? Is it documented somewhere? maybe it would be clearer and safer to make our own array of ('y', 'm', 'd', ...) and iterate on that, and get the values with $interval->$key?

  4. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,79 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +            $output .= ($output ? ' ' : '') . $this->formatPlural($interval->format('%' . $key), '1 year', '@count years', array(), array('langcode' => $langcode));
    

    I think we can just call $interval->$key rather than asking $interval to format itself? or use $value?

  5. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,13 +151,13 @@ public function render(ResultRow $values) {
    -          return $this->dateFormatter->formatInterval($time_diff, is_numeric($custom_format) ? $custom_format : 2);
    +          return $this->dateFormatter->formatDiff($value, REQUEST_TIME, 'UTC', is_numeric($custom_format) ? $custom_format : 2);
             case 'time ago':
    

    I'd be surprised if 'UTC' was the correct time zone to use for this (or the other ones in this area of the patch)?

  6. +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
    @@ -128,4 +128,111 @@ public function testFormatIntervalZeroSecond() {
    +  public function providerTestFormatDiff() {
    

    At some point, some tests with time zones seem like a good idea.

jhodgdon’s picture

Whoops. That first code segment in my previous comment should have been the docs for the new method, where it says something about one timestamp being "larger" than the other. Sorry about the confusion!

Anonymous’s picture

Thank you for your feedback! It's very helpful and I'll look into all of it, but I wanted to comment on 3 right away.

It's a very valid point your are making:
- It works because those properties are public and specified in that order in the \DateInterval implementation.
- Furthermore we actually have 'days' and 'invert' as keys as well. They get skipped because they don't match a case of the switch. I should add a "default; break;" there.
Anyway, we shouldn't be relying on that to be future proof.

tim.plunkett’s picture

#109878: format_interval should support leap-year calculation is a much much older issue, but this issue seems to have more momentum.

Anonymous’s picture

Assigned: » Unassigned

I'm unassigning this for now because I won't have much time to work on it in the coming week and I don't want to be the blocking factor. If anyone wants to work on it, please feel free. If not, I'll assign it back to myself when I have more time next week.

rteijeiro’s picture

Status: Needs work » Needs review
FileSize
13.58 KB
10.2 KB

Addressed a few nitpicks and points 1, 2 and 4 in #28. I think point 3 is answered in #30 and need some feedback about points 5 and 6. UTC is the default timezone, not sure why it's wrong.

Status: Needs review » Needs work

The last submitted patch, 33: 2456521-33.patch, failed testing.

mpdonadio’s picture

Think this patch needs to be adjusted for #2459155: Remove REQUEST_TIME from bootstrap.php.

+++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
@@ -193,6 +199,86 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
+  public function formatDiff($timestamp1, $timestamp2 = REQUEST_TIME, $timezone = 'UTC', $granularity = 2, $langcode = NULL) {
+    $date_timezone = new \DateTimeZone($timezone);
+
+    $date_time1 = new \DateTime();
+    $date_time1->setTimestamp($timestamp1);
+    $date_time1->setTimezone($date_timezone);
+
+    $date_time2 = new \DateTime();
+    $date_time2->setTimestamp($timestamp2);
+    $date_time2->setTimezone($date_timezone);
+
+    $interval = $date_time2->diff($date_time1);

Not sure why a time zone is needed here, when the interval is being calculated off of two timestamps from the same timezone.

mpdonadio’s picture

  1. +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
    @@ -128,4 +128,115 @@ public function testFormatIntervalZeroSecond() {
    +  /**
    +   * Tests the formatDiff method.
    +   *
    +   * @dataProvider providerTestFormatDiff
    +   *
    +   * @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
    +   */
    +  public function testformatDiff($expected, $timestamp1, $timestamp2 = REQUEST_TIME, $timezone = 'UTC', $granularity = 2, $langcode = NULL) {
    +
    

    The docblock here should have a @covers instead of a @see.

  2. +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
    @@ -128,4 +128,115 @@ public function testFormatIntervalZeroSecond() {
    +  /**
    +   * Provides some test data for the format diff test.
    +   */
    +  public function providerTestFormatDiff() {
    +    // This is the fixed request time in the test.
    

    The dataset returned by the provider doesn't cover the $timezone, $granularity, and $langcode parameters. The first line should also read something like "Data provider for testformatDiff()." If the timezone really is needed then there should be a test that shows where it makes a difference.

Anonymous’s picture

Thanks for the progress @rteijeiroj! I've given the timezone issue some thought. As I see it, we have to options:
- Remove the parameter, and make it the responsibility of the calling function to make sure the timezones are the same for both timestamps.
- Accept a second timestamp, so both timestamps can be in different timezones. If we do that, I think we should add them to the $options instead of as separate variables.
I'm in favour of the second option, because it seems to me it would be a great addition to the function. If we do that, we should figure out what the default should be. I think I got "UTC" as a value from somewhere else in core, but I can't recall where that was. A quick search will clarify this.

Re #33: The third bullet was not addressed in #30, but I merely tried to explain why it works as-is. We should definitely change the implementation like @jhodgdon proposed in #28.

Re #35: Thanks for that pointer. We should indeed use (int) $_SERVER['REQUEST_TIME'] to determine the value of the request time.

Re #36: Yes, those are still todo. I think they could be added at random in the existing test data.

And lastly, we shouldn't forget to handle identical timestamps (i.e. with no time difference). There is a @todo tag in the tag for that.

Anonymous’s picture

Assigned: Unassigned »

I'll take a look at the timezone issue.

Anonymous’s picture

I've played around with the timezones in \DateTime a bit. In core, 'UTC' seems to be used as the default, see DateTimePlus::prepareTimezone().

That said, the \DateTime->diff() doesn't seem to take it into account. When adding a timezone setting to a timestamp, the output from \DateTime->format() changes accordingly, but when comparing two timestamps, it doesn't change anything.
With this finding, I think we should remove the option for implementors to add timezones and assume the same timezone for both timestamps. I'll remove them from the patch and produce a new one with the other feedback fixed.

The current todo list looks as follows:
- #28.3: explicitly define the variables to loop over array(’s’, ‘i’, …)
- #28.3: add default to the switch
- #35: change REQUEST_TIME to (int) $_SERVER['REQUEST_TIME']
- #36.1: docblock testformatDiff add @see and @covers
- #36.2: comment providerTestFormatDiffData provider for testformatDiff().
- #36.2: test $granularity
- #36.2: test $langcode
- #37: handle 0

Edit:
- #14.d: change most/all uses in core
- #25: prefix/suffix handling

Anonymous’s picture

FileSize
9.87 KB
14.14 KB

Some work in progress. I addressed the first 5 bullets in the list in my previous comment. I was about to add additional coverage, but the tests started failing for some reason. I couldn't find out why yet.

Todo:
- #36.2: test granularity
- #36.2: test langcode
- #37: handle 0
- #25: prefix/suffix handling
- #14.d: change most/all uses in core

jhodgdon’s picture

Thanks for investigating the time zones! That seems sensible to me -- no time zones then.

I took a look at the latest patch and have some questions/suggestions, but it looks pretty good!

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,95 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   (optional) An associative array with additional options. Following keys
    

    Should say "The following keys..."

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,95 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   can be used:
    +   *   - granularity: An integer value that signals how many different units to
    +   *   display in the string. Defaults to 2.
    +   *   - langcode: A string representing a language code. It is used to
    +   *   translate to a language other than what is used to display the page.
    +   *   Defaults to NULL, which results in the current user language being used.
    +   *
    

    Formatting here: The text on the second line below the - list bullets should be indented 2 spaces.

  3. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,95 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   * @return string
    +   *   A translated string representation of the interval. The string will be
    +   *   prepended with a minus sign if timestamp1 time is after timestamp2.
    +   *
    

    Um... It doesn't seem like prepending with a - is definitely a good idea? Weren't we going to at least have some options here? I doubt that this is the best default.

  4. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,95 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +    // @todo handle 0
    

    What does this mean?

  5. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,95 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +            $output .= ($output ? ' ' : '') . $this->formatPlural($interval->y, '1 year', '@count years', array(), array('langcode' => $options['langcode']));
    

    Would this be easier if we did something like

    $thisoutput = $this->formatPlural...
    

    in the switch statement, and then just one

    $output .= ($output ? ' ' : '') . $thisoutput;
    
  6. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,24 +151,35 @@ public function render(ResultRow $values) {
               return ($time_diff < 0 ? '-' : '') . $this->dateFormatter->formatInterval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2);
    +
             case 'inverse time span':
               return ($time_diff > 0 ? '-' : '') . $this->dateFormatter->formatInterval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2);
    +
             case 'time span':
               return $this->t(($time_diff < 0 ? '%time hence' : '%time ago'), array('%time' => $this->dateFormatter->formatInterval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2)));
    +
    

    Why are we still using formatInterval for these cases?

Anonymous’s picture

Issue tags: +drupaldevdays
FileSize
24 KB
25.21 KB

Let's see what I broke while replacing all occurrences in core.

This patch also adds support for prefix and suffix, and test coverage for granularity, langcode, prefix and suffix options.

#41.1: Ok
#41.2: I'm not sure why it's not ok? It's part of the langcode documentation and a coincidence it starts on a new line.
#41.3: Yes, we changed that, so adjusted comments to reflect that.
#41.4: It means "handle no time difference". Removed the comment, and added to todo list.
#41.5: Ok
#41.6: Because we couldn't pass options yet, but now we can, so changed them as well.

Todo:
- probably look at failing tests
- handle "no time difference"

Anonymous’s picture

Status: Needs work » Needs review

Triggering testbot.

The last submitted patch, 40: 2456521-40.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 42: 2456521-42.patch, failed testing.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
3.51 KB
26.23 KB

Fixed some tests. Let's see what's left.

Anonymous’s picture

Issue tags: -Needs tests
FileSize
1.2 KB
26.39 KB

Added support for when there is no time difference. I'll read the full issue again, but I think we've got everything covered now.

mpdonadio’s picture

+++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
@@ -128,4 +128,147 @@ public function testFormatIntervalZeroSecond() {
+  /**
+   * Tests the formatDiff method.
+   *
+   * @dataProvider providerTestFormatDiff
+   *
+   * @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
+   */

Needs to use @covers instead of @see. Otherwise, I think this looks good. I will do a full review of this tonight.

Anonymous’s picture

Assigned: » Unassigned

In #36.1 @mpdonadio proposed to change the @covers in an @see tag for the unit test. After changing that, all tests started failing, so I rolled it back in this patch. I'm not sure what we should do here.

Edit: should have updated before posting :) The problem is that I don't know where to look for feedback when the tests fail due to an annotation. If you could give a pointer, I'll look at it again.

Anonymous’s picture

FileSize
1.23 KB
26.67 KB

Found how to do the @covers thing in the phpunit documentation.

Also fixing the test failure I introduced in the last patch.

mpdonadio’s picture

When an annotation fails, look for other uses in core.

diff --git a/core/tests/Drupal/Tests/Core/Datetime/DateTest.php b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
index 42d6c66..a9636d6 100644
--- a/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
+++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
@@ -57,7 +57,7 @@ protected function setUp() {
    *
    * @dataProvider providerTestFormatInterval
    *
-   * @see \Drupal\Core\Datetime\DateFormatter::formatInterval()
+   * @covers ::formatInterval
    */
   public function testFormatInterval($interval, $granularity, $expected, $langcode = NULL) {
     // Mocks a simple formatPlural implementation.
@@ -133,7 +133,7 @@ public function testFormatIntervalZeroSecond() {
    *
    * @dataProvider providerTestFormatDiff
    *
-   * @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
+   * @covers ::formatDiff
    */
   public function testformatDiff($expected, $timestamp1, $timestamp2 = NULL, $options = array()) {

With this change, I get the same single failure as #47:

There was 1 failure:

1) Drupal\Tests\Core\Datetime\DateTest::testformatDiff with data set #0 ('0 seconds', 1386756548, 1386756548)
The method returned in the expected formatted diff string.
Failed asserting that null matches expected '0 seconds'.

Anonymous’s picture

FileSize
583 bytes
26.63 KB

Ah yes, I should have thought of that :)

The last submitted patch, 47: 2456521-47.patch, failed testing.

mpdonadio’s picture

You can use the ::method syntax because the main docblock for the class has a @coversDefaultClass in it.

I will do a full review of this tonight, but I will let @jhodgdon have the final say to make sure the docs look good.

mpdonadio’s picture

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +    if(is_null($timestamp2)) {
    +      $timestamp2 = $_SERVER['REQUEST_TIME'];
    +    }
    +
    

    Needs space between if and opening paren.

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +    if(empty($output)) {
    +      $output = $this->t('0 seconds');
    +    }
    +
    +    if($options['prefix'] != '') {
    +      $output = $options['prefix'] . ' ' . $output;
    +    }
    +
    +    if($options['suffix'] != '') {
    +      $output .= ' ' . $options['suffix'];
    +    }
    +
    

    Same, double check other places. See https://www.drupal.org/coding-standards#controlstruct

  3. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +    if($options['prefix'] != '') {
    +      $output = $options['prefix'] . ' ' . $output;
    +    }
    +
    +    if($options['suffix'] != '') {
    +      $output .= ' ' . $options['suffix'];
    +    }
    +
    

    Does this mean that the prefix and suffix can't be translated? Not sure what is best here.

  4. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -148,27 +148,48 @@ public function render(ResultRow $values) {
    -      $time_diff = REQUEST_TIME - $value; // will be positive for a datetime in the past (ago), and negative for a datetime in the future (hence)
    +      $time_diff = $_SERVER['REQUEST_TIME'] - $value; // will be positive for a datetime in the past (ago), and negative for a datetime in the future (hence)
    

    I think these REQUEST_TIME conversions are out of scope here (there were a few other places in the patch, too). You only needed to fix the ones in the unit test, or any code that you add that will be unit tested. The CR doesn't mention it, but using the superglobal is kinda frowned upon, too. Getting the request time via the current Request object is better in non-test code.

mpdonadio’s picture

Status: Needs review » Needs work

OK setting this back to Needs Work, but it is looking really good and close to ready. There are additional uses of DateFormatter::formatInterval(); I attached a report from PhpStorm. The array_map usages should be looked into to see if they should be converted.

jhodgdon’s picture

RE #50 item 3: Presumably prefix/suffix are config somewhere, but we should document that these should be passed in translated.

So... I also took a look at this patch and I'm a bit confused/bothered by parts of it...

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   (optional) A UNIX timestamp, defining the to date and time. If NULL is
    +   *   passed, the current request time is assumed. Defaults to NULL.
    +   * @param array $options
    

    Wording is slightly awkward and inconcise here... can we change this to:

    ... to date and time. NULL (default) indicates the time of the current request.

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   - granularity: An integer value that signals how many different units to
    +   *   display in the string. Defaults to 2.
    +   *   - langcode: A string representing a language code. It is used to
    

    Second line here needs 2 spaces of indentation. Same in the next bullet point.

  3. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   - prefix: A string that is prepended to the output.
    +   *   - suffix: A string that is appended to the output.
    +   *
    

    So we need to say "translated string"

  4. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,109 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   - prefix: A string that is prepended to the output.
    +   *   - suffix: A string that is appended to the output.
    +   *
    

    These should also say the default is empty?

  5. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php
    @@ -87,7 +87,7 @@ public function viewElements(FieldItemListInterface $items) {
    -        $updated = $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $item->value)));
    +        $updated = $this->dateFormatter->formatDiff($_SERVER['REQUEST_TIME'] - $item->value, $_SERVER['REQUEST_TIME'], array('suffix' => 'ago'));
           }
    

    Um, really? I don't think this is right. Shouldn't this just have $item->value as the first arg and the second as NULL? I think the value here is a timestamp, right?

  6. +++ b/core/modules/aggregator/src/Controller/AggregatorController.php
    @@ -127,8 +127,8 @@ public function adminOverview() {
    -      $row[] = ($last_checked ? $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $last_checked))) : $this->t('never'));
    -      $row[] = ($last_checked && $refresh_rate ? $this->t('%time left', array('%time' => $this->dateFormatter->formatInterval($last_checked + $refresh_rate - REQUEST_TIME))) : $this->t('never'));
    +      $row[] = ($last_checked ? $this->dateFormatter->formatDiff($_SERVER['REQUEST_TIME'] - $last_checked, $_SERVER['REQUEST_TIME'], array('suffix' => 'ago')) : $this->t('never'));
    +      $row[] = ($last_checked && $refresh_rate ? $this->dateFormatter->formatDiff($last_checked + $refresh_rate - $_SERVER['REQUEST_TIME'], $_SERVER['REQUEST_TIME'], array('suffix' => 'left')) : $this->t('never'));
           $links['edit'] = [
    

    Same here.

  7. +++ b/core/modules/contact/src/MessageForm.php
    @@ -200,7 +200,7 @@ public function validate(array $form, FormStateInterface $form_state) {
             $form_state->setErrorByName('', $this->t('You cannot send more than %limit messages in @interval. Try again later.', array(
               '%limit' => $limit,
    -          '@interval' => $this->dateFormatter->formatInterval($interval),
    +          '@interval' => $this->dateFormatter->formatDiff($_SERVER['REQUEST_TIME'] + $interval),
             )));
    

    Here, it seems like it should just be formatDiff($interval) shouldn't it?

  8. +++ b/core/modules/contact/src/MessageForm.php
    @@ -200,7 +200,7 @@ public function validate(array $form, FormStateInterface $form_state) {
    -          '@interval' => $this->dateFormatter->formatInterval($interval),
    +          '@interval' => $this->dateFormatter->formatDiff($_SERVER['REQUEST_TIME'] + $interval),
    
    +++ b/core/modules/system/src/Form/CronForm.php
    @@ -101,8 +101,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    -    $status = '<p>' . t('Last run: %cron-last ago.', array('%cron-last' => $this->dateFormatter->formatInterval(REQUEST_TIME - $this->state->get('system.cron_last')))) . '</p>';
    +    $status = '<p>' . $this->dateFormatter->formatDiff($this->state->get('system.cron_last'), $_SERVER['REQUEST_TIME'], array('prefix' => 'Last run:', 'suffix' => 'ago')) . '</p>';
    

    Might just pass in NULL here for second arg?

  9. +++ b/core/modules/user/src/UserListBuilder.php
    @@ -151,10 +151,8 @@ public function buildRow(EntityInterface $entity) {
    +    $row['member_for'] = $this->dateFormatter->formatDiff($entity->getCreatedTime());
    +    $row['access'] = $entity->access ? $this->dateFormatter->formatDiff($entity->getLastAccessedTime(), REQUEST_TIME, array('suffix' => 'ago')) : t('never');
         return $row + parent::buildRow($entity);
    

    Same here. If we have this NULL capability, let's use it?

  10. +++ b/core/modules/views/src/Plugin/views/cache/Time.php
    @@ -141,7 +141,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    -    return $this->dateFormatter->formatInterval($results_lifespan, 1) . '/' . $this->dateFormatter->formatInterval($output_lifespan, 1);
    +    return $this->dateFormatter->formatDiff($results_lifespan, 1) . '/' . $this->dateFormatter->formatDiff($output_lifespan, 1);
    

    What is this 1 as second arg here? Seems wrong?

The rest of the changes have similar questions/issues. Am I misunderstanding something?

Anonymous’s picture

Assigned: Unassigned »
Anonymous’s picture

Status: Needs work » Needs review
FileSize
12.86 KB
25.95 KB

Thank you both for the reviews!

@mpdonadio: Fixed the spaces. And guarding the scope indeed seems like a good idea here :)

@jhodgdon:
No, you are not misunderstanding this. NULL is the right parameter to pass even though the result is the same.
I think I addressed everything, except for #7 because I don't think $interval is a timestamp.

Anonymous’s picture

FileSize
25.95 KB
25.95 KB

I fixed a line in #59 and reuploaded a new patch with the same name, but I don't see the fix. Let's retry with a new name.

Anonymous’s picture

jhodgdon’s picture

Status: Needs review » Needs work

This is getting closer!

I think there are still a couple of problems:

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,112 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   * @return string
    +   *   A translated string representation of the interval.
    +   *
    

    I had to return up here to read the code when I was reviewing the rest of this patch, to figure out the polarity of "from" and "to" in the arguments. Maybe we should add something to this @return docs that explains the polarity, which I think is that if $2 is after $1, the numbers will be positive?

    And maybe the arguments should be called $from and $to rather than $timestamp1 and $timestamp2?

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,112 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +          default:
    +            break;
    +        }
    

    Do we really need this? ;)

  3. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php
    @@ -87,7 +87,7 @@ public function viewElements(FieldItemListInterface $items) {
    -        $updated = $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $item->value)));
    +        $updated = $this->dateFormatter->formatDiff($item->value, NULL, array('suffix' => 'ago'));
           }
    

    Hm. I'm wondering about this now... There could potentially be languages where "@time ago" got translated as "foo @time bar" or even "foo @time", where "foo" and "bar" represent some foreign words/phrases. So maybe passing in a prefix/suffix in these cases is not the right thing to do? I think we should be calling formatDiff and then using it in $this->t() as in the old code instead.

    And similar for other areas of this patch.

  4. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php
    @@ -87,7 +87,7 @@ public function viewElements(FieldItemListInterface $items) {
    +        $updated = $this->dateFormatter->formatDiff($item->value, NULL, array('suffix' => 'ago'));
    

    I think the suffix needs to be t('ago'), unless we're doing t('@time ago') as noted above? Similar for other prefix/suffix except when it is -.

  5. +++ b/core/modules/contact/src/MessageForm.php
    @@ -200,7 +200,7 @@ public function validate(array $form, FormStateInterface $form_state) {
             $form_state->setErrorByName('', $this->t('You cannot send more than %limit messages in @interval. Try again later.', array(
               '%limit' => $limit,
    -          '@interval' => $this->dateFormatter->formatInterval($interval),
    +          '@interval' => $this->dateFormatter->formatDiff($_SERVER['REQUEST_TIME'] + $interval),
             )));
    

    Maybe the correct way to call this would be formatDiff(0, $interval)?

  6. +++ b/core/modules/contact/src/Tests/ContactSitewideTest.php
    @@ -231,7 +231,7 @@ function testSiteWideContact() {
    -    $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'), '@interval' => \Drupal::service('date.formatter')->formatInterval(600))));
    +    $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'), '@interval' => \Drupal::service('date.formatter')->formatDiff(REQUEST_TIME - 600))));
     
    

    Here actually this is a test. We should just pass in "5 minutes" as a literal string in the test, which is the expected result of the formatting?

  7. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,38 @@ public function render(ResultRow $values) {
    +          $options = array(
    +            'granularity' => isset($this->options['granularity']) ? $this->options['granularity'] : 2
    +          );
    +          return $this->t('%time ago', array('%time' => $this->dateFormatter->formatDiff($value, NULL, $options)));
    +
    

    This section of code seems to be doing the right thing with t() and the prefix/suffix, in contrast to most of the rest.

  8. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,38 @@ public function render(ResultRow $values) {
             case 'raw time hence':
    -          return $this->dateFormatter->formatInterval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2);
    +          return $this->dateFormatter->formatDiff($value, NULL, array('granularity' => is_numeric($custom_format) ? $custom_format : 2));
    +
    

    Hm, does this work right? Seems like if time ago passes in $value, NULL, then time hence should pass in NULL, $value [assuming that NULL in param 1 means "get the current request time" -- the method doesn't do that currently but could/should?]

  9. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,38 @@ public function render(ResultRow $values) {
    +          $options = array(
    +            'granularity' => is_numeric($custom_format) ? $custom_format : 2,
    +            'prefix' => ($time_diff < 0 ? '-' : ''),
    +          );
    +          return $this->dateFormatter->formatDiff(abs($time_diff), NULL, $options);
             case 'inverse time span':
    

    This doesn't look right still, see previous review.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
13.74 KB
25.23 KB

- As discussed, we ignore the sign of the result and always return a positive diff. So we need to document that.
- Changed the variable names.
- Removed the prefix/suffix and rolled back all calls in core to use the $this->t() function.

jhodgdon’s picture

Status: Needs review » Needs work

See also #2399211-71: Support all options from views fields in DateTime formatters

Took a look at the latest patch and I think it's looking pretty good! A few comments - and I think we're getting down to the edge cases now...

  1. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/TimestampAgoFormatter.php
    @@ -87,7 +87,7 @@ public function viewElements(FieldItemListInterface $items) {
    -        $updated = $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $item->value)));
    +        $updated = $this->t('@time ago', array('@time' => $this->dateFormatter->formatDiff($item->value)));
           }
    

    So... It's a bit interesting here, what happens for times that are in the future.

    formatInterval() would I think end up saying "0 sec" if $item->value was in the future.

    formatDiff() would end up saying something like "1 year ago" if the time was in the future.

    Hm....

  2. +++ b/core/modules/aggregator/src/Controller/AggregatorController.php
    @@ -127,8 +127,8 @@ public function adminOverview() {
    -      $row[] = ($last_checked ? $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $last_checked))) : $this->t('never'));
    -      $row[] = ($last_checked && $refresh_rate ? $this->t('%time left', array('%time' => $this->dateFormatter->formatInterval($last_checked + $refresh_rate - REQUEST_TIME))) : $this->t('never'));
    +      $row[] = ($last_checked ? $this->t('@last_checked ago', array('@last_checked' => $this->dateFormatter->formatDiff($last_checked))) : $this->t('never'));
    +      $row[] = ($last_checked && $refresh_rate ? $this->t('@time_left left', array('@time_left' => $this->dateFormatter->formatDiff($last_checked + $refresh_rate))) : $this->t('never'));
           $links['edit'] = [
    

    Similar here. I think $last_checked is fine, but $last_checked + $refresh_rate could actually be in the future pretty easily, if it's time to check but cron hasn't run recently. So I think in that last line, it should check to see if $last_checked + $refresh_rate > current time, and if so it would output the "0" option.

    Given both of these ... maybe we need to have an option to our new formatDiff function that says "Treat $from > $to as 0", or something like that, so that we can capture the same behavior as formatInterval() and handle this case?

    Not sure...

  3. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,35 @@ public function render(ResultRow $values) {
    +          return $this->t(($time_diff < 0 ? '%time hence' : '%time ago'), array('%time' => $this->dateFormatter->formatInterval($time_diff, NULL, $options)));
    

    This needs to be two separate t() calls, for proper picking up of the strings in t() for translation. So it should be:
    $time_diff < 0 ? t('%time hence', array()) : t('%time ago', array());

    Same a few lines above.

David_Rothstein’s picture

This issue is actually a duplicate of #445414: DateFormatter::formatInterval() doesn't format monthly intervals correctly but I'll close that one instead (since the patch here is further along). However, some of the tests from that issue might be worth looking at and incorporating here.

I find it strange that formatInterval() supports weeks and this new formatDiff() function doesn't... that is inconsistent. Is there a reason we couldn't just add support for weeks here?:

+          case 'd':
+            $thisoutput = $this->formatPlural($interval->d, '1 day', '@count days', array(), array('langcode' => $options['langcode']));
+            break;

Basically if the number of days ($interval->d) is greater than or equal to 7, seems like we could easily convert that to X weeks and Y days ourselves.

A minor issue, the variable name $thisoutput is nonstandard (words shouldn't run together like that). Maybe call it something like $interval_output instead.

David_Rothstein’s picture

Issue tags: +Needs backport to D7

I think this could be backported to Drupal 7 too.

David_Rothstein’s picture

I actually wound up leaving the other issue open. The patch here doesn't do anything to fix formatInterval()...

There's a limit to how much it can be improved with regard to how it handles months, but it still could be improved a bit and that issue seems like the place to do it.

David_Rothstein’s picture

Title: DateFormatter::formatInterval is deeply flawed » Add DateFormatter::formatDiff() as a non-buggy alternative to DateFormatter::formatInterval() when the start and end of the interval are known
jhodgdon’s picture

Good idea on the weeks, David! Wasn't aware of the other issue, sorry.

And one other thought I had. There is at least one place in this patch where we're passing in formatDiff($interval, 0) to represent "this number of seconds from now".

Due to the months issue (the reason for this in the first place), and even though I am pretty sure I was the one who suggested calling it that way in the first place, I do not think it is correct to do this. It will be fine if $interval is small, but if it goes over a day, due to time zones and months and stuff, calling
formatDiff($interval, 0)
is not really going to be the same as calling
formatDIff(NOW + $interval)

So... need to look at those too.

jhodgdon’s picture

Yeah, so that's interesting [cross-posted with #67].

We should be using formatDiff() if the start/end times are known, and I guess we should be using formatInterval() if it's really a fixed interval that we're trying to represent, like "we want to run cron every 6000 seconds, represent that as a time span"?

[edit, added the rest]

As opposed to "represent the last time cron was run or the next time it will run as a time span", which is a formatDiff() for sure, since it's relative to the current time.

Anonymous’s picture

Assigned: Unassigned »

I'll have a look at all of that.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
12.32 KB
27.01 KB

#64.1 + #64.2: Right, when passing a negative amount of seconds, the result returned would be the default "0 sec”. How about adding a “strict” option to get a similar functionality?
#64.3: Done.
#65: All done.
#69 + #70: Seems sane to me.

Status: Needs review » Needs work

The last submitted patch, 72: add-2456521-72.patch, failed testing.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
946 bytes
27 KB

Fixing the test failures.

David_Rothstein’s picture

I looked over the changes to add support for weeks, and those look good to me - nice.

Anonymous’s picture

FileSize
800 bytes
26.97 KB

Ok, good! Thanks for reviewing.

Just removing a redundant comment here.

jhodgdon’s picture

Status: Needs review » Needs work

This is very close! A few nitpicks and a few substantive comments... and can I just say nice work and very thorough testing?

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,119 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   - strict: A boolean value indicating whether or not the from timestamp
    

    Nitpick: boolean => Boolean

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,119 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *     can be after the to timestamp. If TRUE and from is after to, the
    +   *     result string will be "0 seconds". If FALSE and from is after to, the
    +   *     result string will be the formatted time difference. Defaults to FALSE.
    +   *
    

    I would say $from and $to rather than just "from" and "to" here?

  3. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +199,119 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +          case 'd':
    +            // \DateInterval doesn't support weeks, so we need to calculate them
    +            // ourselves.
    +            $interval_output = '';
    +            if ($interval->d > 6) {
    +              $interval_output .= $this->formatPlural(floor($interval->d / 7), '1 week', '@count weeks', array(), array('langcode' => $options['langcode']));
    +              $granularity--;
    +            }
    +            if ($granularity > 0 && $interval->d % 7 !== 0) {
    +              $interval_output .= ($interval_output ? ' ' : '') . $this->formatPlural($interval->d % 7, '1 day', '@count days', array(), array('langcode' => $options['langcode']));
    +            }
    +            break;
    

    I find code with % to be a bit difficult to read... This could be written a bit differently:

    $days = $interval->d;
    if ($days >= 7) {
      $weeks = floor($days / 7);
      // format the weeks
      $days -= $weeks * 7;
      $granularity--;
    }
    if ($granularity > 0 && $days > 0) {
    ...
    }
    

    I'm not strongly suggesting this though. Matter of preference. ;)

  4. +++ b/core/modules/views/src/Plugin/views/cache/Time.php
    @@ -141,7 +141,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    -    return $this->dateFormatter->formatInterval($results_lifespan, 1) . '/' . $this->dateFormatter->formatInterval($output_lifespan, 1);
    +    return $this->dateFormatter->formatDiff($results_lifespan) . '/' . $this->dateFormatter->formatDiff($output_lifespan);
       }
    

    Ah. The second arg to formatInterval is the granularity, so we'd better pass that into formatDiff to get the same output here? I finally checked what those "1" values were. ;)

  5. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,41 @@ public function render(ResultRow $values) {
             case 'raw time ago':
    -          return $this->dateFormatter->formatInterval($time_diff, is_numeric($custom_format) ? $custom_format : 2);
    +          return $this->dateFormatter->formatDiff($value, NULL, array('granularity' => is_numeric($custom_format) ? $custom_format : 2));
    +
    

    This may be a case where we should use the 'strict' option?

  6. 
    +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,41 @@ public function render(ResultRow $values) {
             case 'time ago':
    -          return $this->dateFormatter->formatInterval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2);
    +          return $this->dateFormatter->formatDiff($value, NULL, array('granularity' => is_numeric($custom_format) ? $custom_format : 2));
    +
    

    This may be another case where we should use the 'strict' option?

  7. 
    +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,41 @@ public function render(ResultRow $values) {
             case 'raw time hence':
    -          return $this->t('%time hence', array('%time' => $this->dateFormatter->formatInterval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2)));
    +          return $this->t('%time hence', array('%time' => $this->dateFormatter->formatDiff($value, NULL, array('granularity' => is_numeric($custom_format) ? $custom_format : 2))));
    +
    

    here too?

  8. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,41 @@ public function render(ResultRow $values) {
             case 'raw time span':
    -          return ($time_diff < 0 ? '-' : '') . $this->dateFormatter->formatInterval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2);
    +          $options = array(
    +            'granularity' => is_numeric($custom_format) ? $custom_format : 2,
    +          );
    +          return ($time_diff < 0 ? '-' : '') . $this->dateFormatter->formatDiff($time_diff, NULL, $options);
             case 'inverse time span':
    

    Hmmmm... I don't think we want to be pssing $time_diff into formatDiff? Shouldn't we be passing in $value like in the other cases? And this probably also needs 'strict'?

  9. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,41 @@ public function render(ResultRow $values) {
             case 'inverse time span':
    -          return ($time_diff > 0 ? '-' : '') . $this->dateFormatter->formatInterval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2);
    +          $options = array(
    +            'granularity' => is_numeric($custom_format) ? $custom_format : 2,
    +          );
    +          return ($time_diff < 0 ? '-' : '') . $this->dateFormatter->formatDiff($time_diff, NULL, $options);
             case 'time span':
    

    Same here?

  10. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,41 @@ public function render(ResultRow $values) {
    +          if ($time_diff < 0) {
    +            $time_span = $this->t('%time hence', array('%time' => $this->dateFormatter->formatInterval($time_diff, NULL, $options)));
    +          }
    +          else {
    +            $time_span = $this->t('%time ago', array('%time' => $this->dateFormatter->formatInterval($time_diff, NULL, $options)));
    +          }
    +          return $time_span;
             case 'custom':
    

    And here?

Anonymous’s picture

Status: Needs work » Needs review
FileSize
6.68 KB
27.23 KB

This could be written a bit differently

Yeah, that's better. If only we could get rid of the magic number 7 as well.

I finally checked what those "1" values were. ;)

:D

This may be a case where we should use the 'strict' option?

That makes sense.

mmmm... I don't think we want to be pssing $time_diff into formatDiff? Shouldn't we be passing in $value like in the other cases?

Very nice catch! This also means we miss proper test coverage here, but we will take care of that in #2399211: Support all options from views fields in DateTime formatters.

Anonymous’s picture

FileSize
27.24 KB
27.24 KB

Forgot a #77.2 occurence.

Anonymous’s picture

FileSize
1005 bytes

Oops.

jhodgdon’s picture

Status: Needs review » Needs work

I don't think we need to worry too much about the magic number of "7 days makes a week". ;)

This is again looking pretty good but I still think there are some problems with the conversions of existing uses of formatInterval():

  1. +++ b/core/modules/user/src/UserListBuilder.php
    --- a/core/modules/views/src/Plugin/views/cache/Time.php
    +++ b/core/modules/views/src/Plugin/views/cache/Time.php
    
    +++ b/core/modules/views/src/Plugin/views/cache/Time.php
    +++ b/core/modules/views/src/Plugin/views/cache/Time.php
    @@ -141,7 +141,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    
    @@ -141,7 +141,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
       public function summaryTitle() {
         $results_lifespan = $this->getLifespan('results');
         $output_lifespan = $this->getLifespan('output');
    -    return $this->dateFormatter->formatInterval($results_lifespan, 1) . '/' . $this->dateFormatter->formatInterval($output_lifespan, 1);
    +    return $this->dateFormatter->formatDiff($results_lifespan) . '/' . $this->dateFormatter->formatDiff($output_lifespan, NULL, array('granularity' => 1));
       }
    

    Actually... This is not really a time difference. It's really a time interval. I don't think that converting it to formatDiff() is the right thing to do here at all. We should leave it as formatInterval(). It has a limited number of options as it's coming from an options form anyway.

  2. +++ b/core/modules/views/src/Plugin/views/cache/Time.php
    @@ -141,7 +141,7 @@ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    -    return $this->dateFormatter->formatInterval($results_lifespan, 1) . '/' . $this->dateFormatter->formatInterval($output_lifespan, 1);
    +    return $this->dateFormatter->formatDiff($results_lifespan) . '/' . $this->dateFormatter->formatDiff($output_lifespan, NULL, array('granularity' => 1));
    

    The first formatDiff() still needs a granularity option. It only got added to the second one.

  3. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,45 @@ public function render(ResultRow $values) {
             case 'raw time hence':
    -          return $this->dateFormatter->formatInterval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2);
    +          return $this->dateFormatter->formatDiff($value, NULL, array('strict' => TRUE, 'granularity' => is_numeric($custom_format) ? $custom_format : 2));
    +
    

    Is this the right polarity? It's supposed to be time hence, meaning that $value is supposed to be in the future. So it seems like we need to call formatDiff(REQUEST, $value) with strict=TRUE?

  4. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,45 @@ public function render(ResultRow $values) {
             case 'time hence':
    -          return $this->t('%time hence', array('%time' => $this->dateFormatter->formatInterval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2)));
    +          return $this->t('%time hence', array('%time' => $this->dateFormatter->formatDiff($value, NULL, array('strict' => TRUE, 'granularity' => is_numeric($custom_format) ? $custom_format : 2))));
    +
    

    Same here as in raw time hence?

  5. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,45 @@ public function render(ResultRow $values) {
             case 'raw time span':
    -          return ($time_diff < 0 ? '-' : '') . $this->dateFormatter->formatInterval(abs($time_diff), is_numeric($custom_format) ? $custom_format : 2);
    +          $options = array(
    +            'granularity' => is_numeric($custom_format) ? $custom_format : 2,
    +            'strict' => TRUE,
    +          );
    +          return ($time_diff < 0 ? '-' : '') . $this->dateFormatter->formatDiff($value, NULL, $options);
             case 'inverse time span':
    

    here I think we do not want strict, sorry if I said we did before? Because we are checking for the polarity of the time diff and then we want the span as output from formatDiff(), right?

  6. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,19 +151,45 @@ public function render(ResultRow $values) {
    +          if ($time_diff < 0) {
    +            $time_span = $this->t('%time hence', array('%time' => $this->dateFormatter->formatInterval($value, NULL, $options)));
    +          }
    +          else {
    

    Ooops, this is calling formatInterval with the formatDiff args, and then also for hence with strict it needs to be (request, $value) not ($value, NULL) right?

    Maybe we need to have the $from arg also accept NULL and be set to REQUEST_TIME?

Anonymous’s picture

Status: Needs work » Needs review
FileSize
5.07 KB
26.45 KB

Re #81:
1. Ok, I already wondered about this, but I couldn't find were I can see this in action yet.
2. Irrelevant as we fully revert that change per bullet 1.
3. "hence" really confuses me. If it means "from now" you are right. Glad we are evaluating that in #2399211: Support all options from views fields in DateTime formatters.
4. Ok.
5. Yes, changed that for raw, inverse and regular time span.
6. Right.

I also added the $from can be NULL suggestion. I'm not sure how to properly test that though, because the NULL timestamp is overruled in the test case with an arbitrary timestamp.

jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

I looked through this latest patch... I think it's all correct now. Phew! Thanks for all the hard work!

Anonymous’s picture

Issue summary: View changes

Wow! Great! Let me find my party hat :D

Thanks for all the reviews!

I added a beta evaluation to the summary.

xjm’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Thanks for the beta evaluation! The documentation in the patch looks great, as does the unit test coverage. Three questions, though:

  1. The issue is marked as a major bug. However, the only test coverage added to the patch that I see is unit tests. What are the functional bugs resolved by the new method?
  2. Why are we keeping the "buggy" method? Why not just replace it if it's broken? (Edit: Maybe the answer to this is somewhere in the issue, but it doesn't appear to be in the summary.)
  3. Following #2, if it's necessary to have both for some reason, can we add an explanation of why we'd use one rather than the other? The @see help but they don't explain the whole picture.

Note that this issue is not unfrozen; it adds a whole API method and changes a bunch of runtime code. So I've removed that line from the summary.

Anonymous’s picture

Issue summary: View changes
Status: Needs work » Needs review

The IS indeed was a bit out of date, so I fixed that.

Re #85:
1. Since we know the start- and enddate, we can give a more accurate representation of several timestamps in core. So we did. As for coverage, the only place I think it's missing is in the timestamp formatters for views, but that will be looked after in #2399211: Support all options from views fields in DateTime formatters.
2. The buggy method can be accurate for small intervals, and we still use it if we don't know the start date and time.
3. I'm really unsure on how to improve this. If you are a developer and look at those doxygens, and you see that one of both methods has an inaccurate result, which one are you going to pick? And the other one if you really have to.

The issue indeed is not unfrozen.

dawehner’s picture

The issue is marked as a major bug. However, the only test coverage added to the patch that I see is unit tests. What are the functional bugs resolved by the new method?

Only? If you look at the test body, you see pretty much no mocking (just for the translation service which is not the problem here).
Instead this patch adds an overwhelming test coverage. I don't see the point in checking basically that the values we render are the ones returned by the date formatter,
we are doing that implicit probably already in multiple places.

Anonymous’s picture

Issue summary: View changes

Exactly. The unit test ensures that the new method works as expected. The webtest should only check if the rendered dates are there, and not what values they have. Those tests do exist (cf. failures in #42), except for the ones mentioned in #86.1.

Minor wording update in the issue summary to reflect that you need both the start and end date to have an interval.

jhodgdon’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

I am updating the issue summary to address the points in #85. I think this should still be RTBC.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

Again, if formatInterval() is so broken, why are we keeping it? From the summary (thanks @jhodgdon):

This is fine for small intervals, up to a couple of weeks. However, if the interval goes over 30 days, it is not possible to say how many "months" is really being represented, even though the function will return a formatted string containing "months" and "years" -- so it is not accurate... However, there are a few uses in Core that are using formatInterval() that do not have two dates that are being compared, so we still need the old function

But the patch doesn't reflect this. At least need to add some documentation to formatInterval() comparing how the two functions work and explaining when formatInterval() should not be used.

jhodgdon’s picture

The patch does, in fact, add documentation to formatInterval:

+   *   A translated string representation of the interval. Note that for
+   *   intervals over 30 days, the output is approximate: a "month" is always
+   *   exactly 30 days, and a "year" is always 365 days. It is not possible to
+   *   make a more exact representation, given that there is only one input in
+   *   seconds.
+   *
+   * @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
    */

Really, what more do you want??????

jhodgdon’s picture

Issue summary: View changes
Status: Needs work » Reviewed & tested by the community

As it stated in the issue summary already, we still need the old function because there are still a few places in Core where it's being used to format an interval that is not the difference between two specific dates. I've added a few more notes to the summary to hopefully clarify this... The bottom line is that the new function is for when there is a difference between two specific dates. The old function is for when there is only a number of seconds and it is not the difference between two specific dates, and if you use the old function, you should simply not rely on it being anything sensible if your interval is longer than 30 days, because it can't be. We've documented this in the patch already...

I'm at my wit's end in figuring out how to fix this so it will be satisfactory. I am once more setting this back to RTBC... sorry I am really not trying to be annoying and we've really tried to document everything correctly here... The code patch is good. The docs are good. The tests are good. We need the new function, and just about every spot in Core is changed to use it, except for a couple of places that do not have two dates to compare, which cannot use the new function.

Hopefully this will do it. If not... please really what more do we need to do???? I just don't get the objection here.

David_Rothstein’s picture

Perhaps a sentence at the top of the formatInterval() documentation would help, that explicitly says something like "if you know the start and end date of the interval, use formatDiff() instead of this function"?

I think it's somewhat already implied by the documentation changes that are already in this patch ("@see formatDiff()" which occurs immediately after an explanation of the function's limitations). But an extra sentence that says it explicitly might not hurt.

jhodgdon’s picture

Status: Reviewed & tested by the community » Needs work

OK, good idea, setting back to needs work. Let's move that whole paragraph about when not to use this up into the main body of the docs instead of in the @return, and make sure it explicitly says "use this other thing instead".

David_Rothstein’s picture

Also, on the topic of functional bugs that this patch fixes, I was going to point to #2430529: "Member for" date format has a funny way of describing 11 years (one of the more obvious manifestations of this bug) but actually I don't think this fixes it yet - the code in user_user_view() is still using formatInterval()?

Similarly, the other one that comes to mind is the Tracker module (e.g. you can see this somewhere around https://www.drupal.org/user/124982/track?page=101 right now, where I have some issues that are listed as last updated "12 months 4 days ago"). But this patch doesn't currently change the Tracker module either...

jhodgdon’s picture

OK. Yes. I thought I had checked this before, but you are right, there are still some uses of formatInterval that should be fixed that are not covered by this patch. Either they have crept in since the last time we checked, or we missed them the last time.

So, double Needs Work.

I could make the next patch but I think I would rather keep myself open for reviewing.
@pjonckiere, are you available? Or alternatively, @David_Rothstein, would you want to review if I make the changes?

David_Rothstein’s picture

I could review it, sure.

Anonymous’s picture

Assigned: » Unassigned

During this week, I won't have any time to address the feedback, so I'll unassign myself for now. I might find some time over the weekend. I'll check back then, but feel free to work on it if you want to.

jhodgdon’s picture

Assigned: Unassigned » jhodgdon

OK I'll take it on.

jhodgdon’s picture

Wow, I guess there were quite a few places that still needed fixing. Here's a new patch. I believe I have updated all uses of formatInterval() to formatDiff that should be updated now. The remaining ones are being used to create options lists, or are genuinely intervals where the start/end date does not appear to be known, or at least it is not obvious how to get it (such as in the Batch API, but hopefully that would not be running for months anyway!).

Status: Needs review » Needs work

The last submitted patch, 101: 2456521-date-diff-formatter-101.patch, failed testing.

jhodgdon’s picture

Priority: Major » Normal
Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
39.71 KB
887 bytes

Test failure is due to a typo in the change I made in UserAdminListingTest at line 94. Here's a new patch. Only that one line in that one file changed.

Also downgrading to a normal bug because of objections raised above, and updating the issue summary a bit again.

David_Rothstein’s picture

Status: Needs review » Needs work

I reviewed the interdiff, plus the whole patch. Found some issues:

  1. I looked through all remaining uses of formatInterval() with this patch applied and only found one that looks like it should still be converted - it's visible in the patch context:
    --- a/core/modules/views/src/Tests/Handler/FieldDateTest.php
    +++ b/core/modules/views/src/Tests/Handler/FieldDateTest.php
    @@ -64,8 +64,8 @@ public function testFieldDate() {
         }
     
         $intervals = array(
    -      'raw time ago' => $this->container->get('date.formatter')->formatInterval(REQUEST_TIME - $time, 2),
    -      'time ago' => t('%time ago', array('%time' => $this->container->get('date.formatter')->formatInterval(REQUEST_TIME - $time, 2))),
    +      'raw time ago' => $this->container->get('date.formatter')->formatDiff($time),
    +      'time ago' => t('%time ago', array('%time' => $this->container->get('date.formatter')->formatDiff($time))),
           // TODO write tests for them
     //       'raw time span' => $this->container->get('date.formatter')->formatInterval(REQUEST_TIME - $time, 2),
     //       'time span' => t('%time hence', array('%time' => $this->container->get('date.formatter')->formatInterval(REQUEST_TIME - $time, 2))),
    

    Not sure why there is commented-out code lying around at all, but I think it should stay up-to-date if it's going to be there.

    There are a couple other places that would in theory be nice to convert but would require too much refactoring to do in this issue, and would not have much practical benefit anyway (as @jhodgdon said in #101).

  2. -   *   (optional) Language code to translate to a language other than what is
    -   *   used to display the page. Defaults to NULL.
    +   *   (optional) Language code, to translate to a language other than what is
    +   *   used to display the page.
    

    Removing that seems a bit out of scope - plus do we want to? If it's going to change at all I think the language used in the other function's documentation ("Defaults to NULL, which results in the current user language being used") makes more sense.

  3. --- a/core/modules/aggregator/src/Controller/AggregatorController.php
    +++ b/core/modules/aggregator/src/Controller/AggregatorController.php
    @@ -127,8 +127,8 @@ public function adminOverview() {
           $row[] = $this->formatPlural($entity_manager->getStorage('aggregator_item')->getItemCount($feed), '1 item', '@count items');
           $last_checked = $feed->getLastCheckedTime();
           $refresh_rate = $feed->getRefreshRate();
    -      $row[] = ($last_checked ? $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $last_checked))) : $this->t('never'));
    -      $row[] = ($last_checked && $refresh_rate ? $this->t('%time left', array('%time' => $this->dateFormatter->formatInterval($last_checked + $refresh_rate - REQUEST_TIME))) : $this->t('never'));
    +      $row[] = ($last_checked ? $this->t('@last_checked ago', array('@last_checked' => $this->dateFormatter->formatDiff($last_checked))) : $this->t('never'));
    +      $row[] = ($last_checked && $refresh_rate ? $this->t('@time_left left', array('@time_left' => $this->dateFormatter->formatDiff($last_checked + $refresh_rate, NULL, array('strict' => TRUE)))) : $this->t('never'));
    

    The second one has backwards inputs to formatDiff (and it matters because of the 'strict').

    Also, changing "%time" to "@time_left" seems out-of-scope here, although it probably does make the UI more consistent - I guess it's fine :)

  4. --- a/core/modules/contact/src/Tests/ContactSitewideTest.php
    +++ b/core/modules/contact/src/Tests/ContactSitewideTest.php
    @@ -231,7 +231,7 @@ function testSiteWideContact() {
         }
         // Submit contact form one over limit.
         $this->submitContact($this->randomMachineName(16), $recipients[0], $this->randomMachineName(16), $id, $this->randomMachineName(64));
    -    $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'), '@interval' => \Drupal::service('date.formatter')->formatInterval(600))));
    +    $this->assertRaw(t('You cannot send more than %number messages in 10 min. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'))));
    

    Not sure this is a good change. I would leave the old code as is, personally.

  5. --- a/core/modules/system/system.tokens.inc
    +++ b/core/modules/system/system.tokens.inc
    @@ -68,7 +68,7 @@ function system_token_info() {
       );
       $date['since'] = array(
         'name' => t("Time-since"),
    -    'description' => t("A date in 'time-since' format. (%date)", array('%date' => \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - 360, 2))),
    +    'description' => t("A date in 'time-since' format. (%date)", array('%date' => \Drupal::service('date.formatter')->formatDiff(REQUEST_TIME - 360))),
    

    This does not look right...

  6.          case 'time ago':
    -          return $this->t('%time ago', array('%time' => $this->dateFormatter->formatInterval($time_diff, is_numeric($custom_format) ? $custom_format : 2)));
    +          $options = array(
    +            'granularity' => isset($this->options['granularity']) ? $this->options['granularity'] : 2,
    +            'strict' => TRUE,
    +          );
    +          return $this->t('%time ago', array('%time' => $this->dateFormatter->formatDiff($value, NULL, $options)));
    

    Was changing $custom_format to $this->options['granularity'] really intentional here? (None of the other nearby code changed.)

  7. --- a/core/modules/views/src/Plugin/views/field/TimeInterval.php
    +++ b/core/modules/views/src/Plugin/views/field/TimeInterval.php
    @@ -87,7 +87,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
        */
       public function render(ResultRow $values) {
         $value = $values->{$this->field_alias};
    -    return $this->dateFormatter->formatInterval($value, isset($this->options['granularity']) ? $this->options['granularity'] : 2);
    +    return $this->dateFormatter->formatDiff($value, NULL, array('granularity' => isset($this->options['granularity']) ? $this->options['granularity'] : 2));
    

    This does not look right.... perhaps this is a case that needs to stay at formatInterval?

  8. +  public function testformatDiff($expected, $timestamp1, $timestamp2 = NULL, $options = array()) {
    ....
    +    $this->assertEquals($expected, $this->dateFormatter->formatDiff($timestamp1, $timestamp2, $options), 'The method returned in the expected formatted diff string.');
    

    The grammar in the assertion message is wrong. Also is a custom assertion message necessary? I am not sure what the fallback behavior is for this in Drupal 8, but in Drupal 7 it is usually better not to pass a custom assertion message to $this->assertEqual() since the default one shows both values and therefore is more helpful for debugging if the test ever fails.

  9. -      $row[] = ($last_checked ? $this->t('@time ago', array('@time' => $this->dateFormatter->formatInterval(REQUEST_TIME - $last_checked))) : $this->t('never'));
    .....
    +      $row[] = ($last_checked ? $this->t('@last_checked ago', array('@last_checked' => $this->dateFormatter->formatDiff($last_checked))) : $this->t('never'));
    
    -    $row['access'] = $entity->access ? $this->t('@time ago', array(
    ....
    +    $row['access'] = $entity->access ? $this->t('@last_access ago', array('@last_access' => $this->dateFormatter->formatDiff($entity->getLastAccessedTime()))) : t('never');
    

    Does changing the "@time ago" string provide any benefit here? Seems to me like it would just create more work for translators (they now have 3 strings to translate that all mean the exact same thing).

And a couple bigger questions:

  1. @@ -193,6 +201,127 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
       }
     
       /**
    +   * Formats a time interval between two timestamps.
    +   *
    +   * @param int $from
    +   *   A UNIX timestamp, defining the from date and time. NULL (default)
    +   *   indicates the time of the current request.
    +   * @param int $to
    +   *   (optional) A UNIX timestamp, defining the to date and time. NULL
    +   *   (default) indicates the time of the current request.
    

    I'm not sure it is worth allowing these parameters to be optional. The fallback behavior saves a little typing but I think it also makes the code harder to read. Here are a couple examples:

    -            '!time' => \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $node->last_activity),
    +            '!time' => \Drupal::service('date.formatter')->formatDiff($node->last_activity),
    

    The first is easier to understand. Using formatDiff($node->last_activity, REQUEST_TIME) would make the new version as easy to understand as the old one.

    -    $tests['[date:since]'] = $date_formatter->formatInterval(REQUEST_TIME - $date, 2, $this->interfaceLanguage->getId());
    +    $tests['[date:since]'] = $date_formatter->formatDiff($date, NULL, array('langcode' => $this->interfaceLanguage->getId()));
    

    Same here - and in this case it doesn't save much at all since you have to specify the second parameter either way.

    Any thoughts on whether it's really worth having those parameters be optional?

  2. +   *   - strict: A Boolean value indicating whether or not the $from timestamp
    +   *     can be after the $to timestamp. If TRUE and $from is after $to, the
    +   *     result string will be "0 seconds". If FALSE and $from is after $to, the
    +   *     result string will be the formatted time difference. Defaults to FALSE.
    

    I think we should consider making TRUE the default.

    Looking through the patch, there are very few places where FALSE is definitely what the code wants to do (some examples from core/modules/views/src/Plugin/views/field/Date.php are the only ones I saw).

    For most of the others, it doesn't matter much either way - for example, in a case like this:

    +    $tests['[node:created:since]'] = \Drupal::service('date.formatter')->formatDiff($node->getCreatedTime(), NULL, array('langcode' => $this->interfaceLanguage->getId()));
    

    the node creation time is never in practice expected to be later than the current time. But if for some reason it wound up that way, I think having 'strict' set to TRUE gives you the behavior you'd want to display in this case too?

    Changing the default behavior of 'strict' to TRUE would also make formatDiff()'s default behavior more consistent with formatInterval().

jhodgdon’s picture

Status: Needs work » Needs review
FileSize
39.3 KB
12.61 KB

Wow, thanks for the detailed and careful review! Much appreciated.

Here is a new patch... hopefully addressing all of your review points. A few notes -- everywhere else I agreed with what you said and hopefully made good changes:

item 1: I decided to remove the commented-out code, since our coding standards forbid this. Instead turned it into a proper @todo and filed an issue to track it: #2488844: Write tests for raw time span and time span formatters in Views

item 2: Fixed both functions' $langcode docs to be more precise.

item 3: I think I agree with you that "strict" should be the default, given that we've had formatInterval with that default for ages and ages. I also don't think it matters in 99% of the cases, because the usual use of this is "pass in a date in the past and format it as ... ago", for things like last time cron ran, node update time, last comment time, etc. But I agree that is how this new function should be. So, that's been updated. Most of the uses of the function didn't change though, since we were not too careful about strict anyway, and most of them were expected to be times in the past anyway.

item 4: Really, I think all the tests should change to test for a definite output string rather than testing output by comparing to another call to the function that is being tested, which seems a bit circular. I suggested it in comment #62 item 6... would prefer to leave it as it is. If you feel strongly about it, I'm willing to change it.

item 5: This is providing a sample formatted piece of output for the "since" date token type. I think that the previous code was weird/incorrect. The changed code will give us output of "10 min", which seems reasonable; the previous one would have done something like "30 years 10 days etc.", because it was passing in REQUEST_TIME in as an interval, which was just plain wrong. Obviously there was no test...

item 9: Excellent point. Went back and fixed all the time strings to be as similar as possible, using "@time" as the placeholder as much as possible for consistency.

item 10: The reason we did this was an earlier review comment that we want to avoid having REQUEST_TIME all over our code. So at least this way it is confined to this one function, for the most part. I think it should stay.

item 11: This is the same as item 3 and I agree. The change has been made in the new patch.

David_Rothstein’s picture

Status: Needs review » Reviewed & tested by the community

I looked over the new version, and this looks ready to go to me. Good call on item 5; that does appear to be a bugfix after all.

I am still skeptical of $this->formatDiff($timestamp) and similar. It is a diff, so providing two timestamps should be expected. The default behavior when only one is provided just isn't obvious, and the current code already uses REQUEST_TIME in all these places anyway. But if that's the consensus so far, I'll push this to RTBC and see what others think.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 105: 2456521-date-diff-formatter-105.patch, failed testing.

mpdonadio’s picture

Status: Needs work » Needs review
FileSize
39.32 KB

Reroll w/ manual merge in CommentTokenReplaceTest to choose the hunk from the patch.

David_Rothstein’s picture

Status: Needs review » Needs work

The manual merge is incorrect - it reverts the new "getChangedTimeAcrossTranslations" code:

-    $tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $comment->getCreatedTime(), 2, $language_interface->getId());
-    $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $comment->getChangedTimeAcrossTranslations(), 2, $language_interface->getId());
+    $tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatDiff($comment->getCreatedTime(), NULL, array('langcode' => $language_interface->getId()));
+    $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatDiff($comment->getChangedTime(), NULL, array('langcode' => $language_interface->getId()));
mpdonadio’s picture

Status: Needs work » Needs review
FileSize
39.34 KB
1.27 KB

Good catch. I didn't notice that. For reference, my patch reverted a portion of #2428795: Translatable entity 'changed' timestamps are not working at all. This should fix that.

jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

Thanks! I have carefully verified that the patch in #110 is a good reroll of the RTBC patch in #105, with the changes from #2428795: Translatable entity 'changed' timestamps are not working at all mentioned in #109 merged in correctly.

David_Rothstein’s picture

Status: Reviewed & tested by the community » Needs review

Agreed, #110 looks good now.

Copying my comment from above, for easy access:

I am still skeptical of $this->formatDiff($timestamp) and similar. It is a diff, so providing two timestamps should be expected. The default behavior when only one is provided just isn't obvious, and the current code already uses REQUEST_TIME in all these places anyway. But if that's the consensus so far, I'll push this to RTBC and see what others think.

David_Rothstein’s picture

Status: Needs review » Reviewed & tested by the community

Oops :)

xjm’s picture

+++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
@@ -163,17 +163,25 @@ public function format($timestamp, $type = 'medium', $format = '', $timezone = N
+   * input in seconds. If you are formatting an interval between two specific
+   * timestamps, use \Drupal\Core\Datetime\DateFormatter::formatDiff() instead.

Yeah this was exactly the missing piece for me, docs-wise. Thanks! This also explains why there's not a functional bugfix text per se -- there is no specific bug, just the old method doesn't always give quite the expected results. (The unit test coverage is extremely thorough.)

I wonder if we could also add one sentence to formatDiff() to the effect that if you are missing a start or end time, you can use formatInterval() instead for a less accurate result? (This is less important I guess since formatInterval() has the details and they both reference each other).

I am still skeptical of $this->formatDiff($timestamp) and similar. It is a diff, so providing two timestamps should be expected. The default behavior when only one is provided just isn't obvious, and the current code already uses REQUEST_TIME in all these places anyway. But if that's the consensus so far, I'll push this to RTBC and see what others think.

So in MySQL, with DATEDIFF() and TIMEDIFF(), both are required. PHP's DateDiff also doesn't make either parameter optional.

PHP functions like strotime() implicitly calculate a difference and use the current time as the default time to calculate a difference from, so I can see where the pattern in this patch came from, but I also see @David_Rothstein's point. (I think it might also make the difference between formatInterval() and formatDiff() less clear -- because if formatDiff() requires two arguments (which is the only reason you'd use formatInterval()), but then doesn't actually "require" them...)

However, to @jhodgdon's point about having REQUEST_TIME all over the place, I started counting how many we'd have to change and... it's like, all of them.

Could we set the default value in the method declaration to REQUEST_TIME instead of NULL to make it explicit? Or is that a bad idea because global constants or something?

Or, maybe we could provide thin wrappers with each argument as null? Like a formatDiffBefore() and formatDiffSince(). Or formatUntil() and formatSince(). Um, hopefully someone else could suggest better names. but it would be like:

public function formatTimeSince($start_time, $options = array()) {
  $this->formatDiff($start_time, NULL, $options);
}

Thoughts? (If others don't think this makes sense I'll go ahead and commit this as-is (once I do a complete code review of the latest patch, anyway). We could also do that as a followup. Leaving at RTBC now but giving a chance for more feedback on that before it goes in.

Aside: this also made me contemplate whether formatInterval() and formatDiff() were the right names, but they are parallel to the PHP method names, so that probably makes sense.

Anonymous’s picture

Great progress!

Re #112:

I am still skeptical of $this->formatDiff($timestamp) and similar. It is a diff, so providing two timestamps should be expected.

and re #114:

Could we set the default value in the method declaration to REQUEST_TIME instead of NULL to make it explicit? Or is that a bad idea because global constants or something?

I get the concern, but I agree with @jhodgdon that we don't want to add REQUEST_TIME all over the place. Actually, we try to avoid using REQUEST_TIME altogether, see #2459155: Remove REQUEST_TIME from bootstrap.php. It was a pointer to that issue by @mpdonadio (#35) that made me change REQUEST_TIME as default, to NULL.

If we choose to change it once more, we should do it properly (i.e. inject the RequestStack, fetch the request object and look up the time there). That would mean more disruption and doesn't allow us to change the default values. So I'd leave it as-is.

Or, maybe we could provide thin wrappers with each argument as null? Like a formatDiffBefore() and formatDiffSince(). Or formatUntil() and formatSince().

On the one hand it improves DX for the simple use cases, but on the other hand developers need to call the formatDiff() function anyway if they want to change the granularity or other options. If we also pass the $options array, it might be worth doing it. I'm curious for other opinions as well, but after reading the IS again this discussion feels out of scope, so I think we should address it in a follow-up.

xjm’s picture

On the one hand it improves DX for the simple use cases, but on the other hand developers need to call the formatDiff() function anyway if they want to change the granularity or other options. If we also pass the $options array, it might be worth doing it. I'm curious for other opinions as well, but after reading the IS again this discussion feels out of scope, so I think we should address it in a follow-up.

Sorry, I meant that the wrappers would also receive these options. I'll update my snippet to make that clear.

xjm’s picture

Thanks @pjonckiere.

We could also make the thin wrappers provide the request time by default, but make formatDiff() expect it. So that could mean injecting an additional dependency that those two methods use internally, but that's not really disruptive. It'd be like:

public function formatTimeSince($start_time, $options = array()) {
  $end_time = $this->request->getRequestTime(); // This is pseudocode.
  $this->formatDiff($start_time, $end_time, $options);
}

public function formatTimeUntil($end_time, $options = array()) {
  $start_time = $this->request->getRequestTime(); // This is pseudocode.
  $this->formatDiff($start_time, $end_time, $options);
}

public function formatDiff($from, $to, $options = array()) {
  // No magic defaults here for null values.

  $options += array(
      'granularity' => 2,
      'langcode' => NULL,
      'strict' => TRUE,
    );

  // ...
}

I think that's actually the better DX, but there would be a BC break between this issue and a followup for that if we chose to scope it to a followup (which is a case for changing it in this patch). @David_Rothstein, @jhodgdon, @mpdonadio, thoughts?

mpdonadio’s picture

I was coming here to leave a quick comment before I realized @xjm had left #117.

I like the thin wrapper idea. I am working on two timestamp related issues where having these would make code more readable. I also like isolating developers from details about low level functions, when possible. Given the choice between doing it as a followup or in this issue, I think it makes more sense to do it as part of this issue rather than touching all of the same places in code again.

I also think that we should be injecting the current request object instead of relying on the superglobal, which @xjm also alluded to with her pseudocode. That was going to be my original comment here.

mpdonadio’s picture

OK, didn't want to disturb RTBC on this, but a proof-of-concept of adding DI to this is at https://www.drupal.org/node/2371491#comment-9959177

However, I am not 100% sure if the mocking on the request stack is bulletproof. It may need to use Request::create(), and pass in a $server argument with a preset REQUEST_TIME to make the test reliable.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
@@ -128,4 +128,167 @@ public function testFormatIntervalZeroSecond() {
+    if ($timestamp2 == $_SERVER['REQUEST_TIME'] || is_null($timestamp2)) {

Why do we need to test if $timestamp2 == $_SERVER['REQUEST_TIME']?

Also I like @xjm's ideas in #117. The D8 and D7 versions do not need to look alike imo.

mpdonadio’s picture

Status: Needs work » Needs review
FileSize
42.99 KB
4.89 KB

OK, since this got set back, here is the first step, which is to inject the request stack so we can get the request time from the current request and avoid the superglobal.

And as I mentioned above, I think the mocking of the request stack in the unit test may need to use `Request::create()` and pass in a $server array with a consistent REQUEST_TIME to make the tests predictable.

Status: Needs review » Needs work

The last submitted patch, 121: interdiff-110-121.patch, failed testing.

Anonymous’s picture

Re #117:

there would be a BC break between this issue and a followup

I did not think of that. Reason enough to do it here.

Re #119/#121:

For the code, that is exactly what I was thinking.

For the test, I was wondering how it could be green. I think it's because the default timestamps from and to are never tested. The first request time is always passed, and the second one is either passed or fixed. We will need to expand coverage.

It may need to use Request::create(), and pass in a $server argument with a preset REQUEST_TIME to make the test reliable.

I agree we shouldn't rely on a changing variable in a test, so fixing it seems like the right approach to me.

Re #120:

IIRC it's legacy, so that check can be safely removed.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
16.93 KB
45.78 KB

This patch:
- Fixes the feedback from #120.
- Makes the $from and $to parameters required in the formatDiff() method and changes the unit test accordingly.
- Introduces a formatTimeSince() method as proposed in #117 with a very limited unit test. This also demonstrates how I would set the request time fixed.

Now, for the newly added test function, I'm wondering if what I do here is correct. Since it's a unit test, we probably want to only test formatTimeSince() without running through formatDiff() and it's dependencies?

Anonymous’s picture

mpdonadio’s picture

Unit testing that the thin wrappers produce the same results as the workhorse should be sufficient. You are really testing that the default value for the gets pulled out properly gets set properly from the request object.

jhodgdon’s picture

Assigned: jhodgdon » Unassigned

I'm not really excited about this new direction but OK. Please someone update the summary.

Status: Needs review » Needs work

The last submitted patch, 124: add-formatDiff-2456521-124.patch, failed testing.

mpdonadio’s picture

@jhodgdon, can you explain what you don't like about the new direction? Personally, I don't see this as a major change, and will result in better DX.

This should eventually have a change record to let everyone know about the new methods.

Anonymous’s picture

Assigned: Unassigned »
Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
4.67 KB
45.67 KB

I improved on the test coverage by mocking the DateFormatter::formatDiff() method and only testing the new method. The mocked RequestStack only returns the expected value when the parameters are the ones we expect, so I think that should be enough.

As for the wrappers, it was proposed to add two convenience methods. Since formatDiff() is always positive, I think that a single one should suffice. So I just renamed it to be more clear and left it at that.

I'll look into the newly introduced test failures now. They are caused by making the parameters of formatDiff() obligatory without touching the coverage.

Edit: minor clarification

Status: Needs review » Needs work

The last submitted patch, 130: add-2456521-130.patch, failed testing.

jhodgdon’s picture

Ignore me, I'm feeling better about this direction today. I think I was just reacting like "Gee whiz, we were almost done let's just get it IN already" or something yesterday. Long day.

So, I took a look at the current patch. I disagree - if we're going this way, we need to have two wrapper functions, because of the 'strict' default for formatDiff. So you need to be able to pass in a date/time in the past or future and still use 'strict'. You can't do that with just one wrapper function... right?

Also, you need to look at the docs for the wrapper function. They are still referring to $from and $to and other stuff from formatDiff(), so they need to be fixed, so that it's clear whether they're expecting a time in the past or in the future, and so that they refer to the args of this function.

Anonymous’s picture

Assigned: » Unassigned
Status: Needs work » Needs review
FileSize
20.62 KB
46.14 KB

Posting a work in progress which is not ready for review yet. I fixed the syntax issues, but didn't have time enough to run all tests and check all places where it is displayed. I am however curious what testbot thinks of it.

Re #132:
As for the wrappers I agree. Strict should work both ways (strict in the future, strict in the past). I realised that while working on this patch.
Ah yes, I copy pasted that a bit too eagerly. Will fix.

So as I currently see it, todos are:
- rename the current wrapper method again (IIRC choosing the one with strict in the past will be the best choice, since it's used more often in core)
- add a second wrapper
- add coverage for that wrapper
- revisit all occurrences
- adjust the change record

I'll try to come back here tomorrow evening. Unassigning as to not block in case there are other takers.

Anonymous’s picture

FileSize
26.76 KB
48.43 KB

All of the above except for the CR. Do we need a new one or can we update one?

mpdonadio’s picture

Since this is the issue that adds the new methods, there shouldn't be an existing CR to update. So, a new one should be made and kept in Draft status until this gets committed.

I'll try to review this when I can.

jhodgdon’s picture

Status: Needs review » Needs work

I took a careful look at the latest patch. Looking good! Thanks for all the work (again)!!!

I had a few minor suggestions on the API doc blocks, and one code question:

  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -116,8 +127,8 @@ public function __construct(EntityManagerInterface $entity_manager, LanguageMana
    +   *   (optional) Language code to translate to. NULL (default) means to use
    +   *   the user interface language for the page.
        *
    

    This is very clear. But...

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +212,179 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   *   - langcode: A string representing a language code. It is used to
    +   *     translate to a language other than what is used to display the page.
    +   *     Defaults to NULL, which results in the current user language being
    +   *     used.
    +   *   - strict: A Boolean value indicating whether or not the timestamp can be
    

    This is not so clear. There is not just one "language to display the page", there is sometimes a selected UI language and possibly a different Content language, and "user language" in the second sentence is definitely not right (implies a languge choice of the user). So we need to be clear here.

    It should probably either match the previous examples, or be changed to say:

    langcode: The language code for the language used to format the date. Defaults to NULL, which results in the user interface language for the page being used.

    This same wording change needs to be done in the other doc blocks too... they all should match.

  3. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -193,6 +212,179 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) {
    +   * @see \Drupal\Core\Datetime\DateFormatter::formatDiff()
    +   */
    +  public function formatTimeDiffSince($timestamp, $options = array()) {
    +    $request_time = $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
    

    We should probably have @see links between all three of the formatDiff functions?

  4. +++ b/core/modules/locale/locale.pages.inc
    @@ -119,6 +119,6 @@ function template_preprocess_locale_translation_update_info(array &$variables) {
    -  $variables['time'] = \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $last);
    +  $variables['time'] = \Drupal::service('date.formatter')->formatTimeDiffSince($last);
       $variables['link'] = \Drupal::l(t('Check manually'), new Url('locale.check_translation', array(), array('query' => \Drupal::destination()->getAsArray())));
    

    I love how much more readable this is. ;)))))

  5. +++ b/core/modules/views/src/Plugin/views/field/Date.php
    @@ -151,24 +151,33 @@ public function render(ResultRow $values) {
             case 'time hence':
    -          return $this->t('%time hence', array('%time' => $this->dateFormatter->formatInterval(-$time_diff, is_numeric($custom_format) ? $custom_format : 2)));
    +          return $this->t('%time hence', array('%time' => $this->dateFormatter->formatTimeDiffUntil($value, array('granularity' => is_numeric($custom_format) ? $custom_format : 2, 'strict' => FALsE))));
    +
    

    Remind me why we put strict=FALSE here? Seems like that's not right?

Anonymous’s picture

Assigned: Unassigned »
Issue summary: View changes
Issue tags: -Needs change record

Re #135: Thanks for clearing that up. I wasn't sure whether or not we could also append it to a similar CR. The "Date renamed to DateFormatter" to be exact.

Anyway, here goes: https://www.drupal.org/node/2497341

IS updates:
- strikethrough old todos
- added the CR to the api changes section
- added todo "commit" and "publish CR"

Onward with #136 now.

Anonymous’s picture

Assigned: » Unassigned
Status: Needs work » Needs review
FileSize
5.98 KB
48.58 KB

Re #136:
- 1/2: I did not even realise that. Fixed.
- 3: Agreed. Fixed.
- 4: I thought exactly the same a couple of times when I went through all occurrences :)
- 5: That was the one that made me realise we needed two wrappers instead of one. And afterwards it seems that I failed to fix it properly. Nice catch!

jhodgdon’s picture

Note: #2488844: Write tests for raw time span and time span formatters in Views just got to RTBC and will conflict with this, because it (a) adds a bunch more formatInterval stuff and (b) is the issue about removing the @todo that was added to this patch in comment #105, so it changes those same lines.

So this may be a "race to commit" problem. Oh well, they're both good patches, this is not a terrible problem to have. ;)

Anyway adding this comment then I'll review...

[Sorry, edit: fixed issue link here]

jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

Great -- I think the code and docs in the patch all look good now. Thanks!

Just to be sure nothing new was needed, I applied this patch and grepped through the code for remaining uses of formatInterval. I checked them all over:

a) Various form builders (and their tests) - used to create option lists for intervals for cron and things like that, example:

core/lib/Drupal/Core/Block/BlockBase.php:    $period = array_map(array(\Drupal::
service('date.formatter'), 'formatInterval'), array_combine($period, $period));

These are actually intervals, so we cannot convert them to use formatDiff or the wrappers.

b) core/modules/views/src/Plugin/views/field/TimeInterval.php -- this is a Views field handler that apparently takes a time interval in seconds and uses formatInterval to format it as an interval. As it has no start/end dates, needs to still use formatInterval. Who knows if this is actually being used anywhere anyway? But it's correct as it is.

c) Batch UI - formats the time elapsed for the request. This is OK to stay as a formatInterval and would require a rewrite of the Batch UI to change it anyway (unlikely to be longer than a few minutes in any case, so formatInterval is accurate there).

That's it, so that looks good too.

The change record looks good to me too. I don't know if we actually need change records for new API functions, but it cannot really hurt.

Let's ship this!

Xano’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -116,8 +127,8 @@ public function __construct(EntityManagerInterface $entity_manager, LanguageMana
        * @param string $langcode
    

    If NULL is allowed, this should be @param string|null $langcode.

  2. +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
    @@ -163,17 +174,26 @@ public function format($timestamp, $type = 'medium', $format = '', $timezone = N
        * @param string $langcode
    

    Same here.

  3. +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
    @@ -38,22 +39,42 @@ class DateTest extends UnitTestCase {
    +   * @var \PHPUnit_Framework_MockObject_MockObject
    

    This is not just a PHPUnit stub. It is also a date formatter object. The type should be defined as both, separated by a pipe character.

  4. +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php
    @@ -38,22 +39,42 @@ class DateTest extends UnitTestCase {
    +   * Tests the formatInterval method.
    

    Use @covers instead of or in addition to a description.

I would personally also like to see all associative array definitions to be multiline for increased readability. This is not in the coding standards, so this feedback is not why I moved the issue back to needs work, but in my experience this improvement increases maintainability.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
2.34 KB
49.26 KB

@jhodgdon: Thanks for the pointer. I started following that issue, and I'll reroll if needed. And thanks for the review!

@xano: Thanks for the review as well. I adjusted the docblocks. I didn't touch the arrays for now, since I'm not sure where to do it and where not. Core doesn't have too much of them, and in the tests I don't always see the point. Would it be an idea to just change the ones we find in core?

jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

Thanks! I believe that Xano's comments in #141 have been addressed, so setting back to RTBC. Xano: feel free to disagree. ;)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 142: add-2456521-142.patch, failed testing.

mpdonadio’s picture

Issue tags: +Needs reroll

Patch no longer applies.

jhodgdon’s picture

Right, #2488844: Write tests for raw time span and time span formatters in Views got committed, so this needs some updates, as noted in comment #139. It will not be a straight reroll, because I think that patch added some more calls to formatInterval() that may need to be fixed up.

mpdonadio’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
49.86 KB

(this was crossposted with #146; this just addresses the reroll, not any new uses of formatInterval)

Applied add-2456521-142.patch against ba05c7401d9cb0c76b66793f517ccc03d0d9e474 (the commit before the one that broke this), then rebased:

First, rewinding head to replay your work on top of it...
Applying: add-2456521-142.patch
Using index info to reconstruct a base tree...
M	core/lib/Drupal/Core/Datetime/DateFormatter.php
M	core/modules/user/user.module
M	core/modules/views/src/Tests/Handler/FieldDateTest.php
M	core/tests/Drupal/Tests/Core/Datetime/DateTest.php
Falling back to patching base and 3-way merge...
Auto-merging core/tests/Drupal/Tests/Core/Datetime/DateTest.php
CONFLICT (content): Merge conflict in core/tests/Drupal/Tests/Core/Datetime/DateTest.php
Auto-merging core/modules/views/src/Tests/Handler/FieldDateTest.php
CONFLICT (content): Merge conflict in core/modules/views/src/Tests/Handler/FieldDateTest.php
Auto-merging core/modules/user/user.module
Auto-merging core/lib/Drupal/Core/Datetime/DateFormatter.php
CONFLICT (content): Merge conflict in core/lib/Drupal/Core/Datetime/DateFormatter.php
Failed to merge in the changes.
Patch failed at 0001 add-2456521-142.patch

Not the easiest hand merge, but I think it is correct. Let's see what the Mr TestBot says.

jhodgdon’s picture

Do you want to make a quick check through the patch from the other issue and see if there are more formatInterval() calls to update, before I review this new patch?

mpdonadio’s picture

Yeah, we should wait until someone can do a pass for formatInterval() again. TestBot is also being slow tonight, too...

Status: Needs review » Needs work

The last submitted patch, 147: add-2456521-146.patch, failed testing.

Anonymous’s picture

Based on #147 I looked for the patches that broke this and then looked over the patch.

http://cgit.drupalcode.org/drupal/commit/?id=4e3e39cbf5fc393cf23b32337d3...
DateFormatter -> accept everything (new function + comment fix) -> OK
DateTest -> accept setup change & fix covers ::formatInterval -> OK

http://cgit.drupalcode.org/drupal/commit/?id=91504462cd0efb5d3bc3330441d...
FieldDateTest -> accept both changes, but make them use new functions -> NOK (see below)

Not sure what commit broke it:
user.module -> one line change in this patch -> OK

+++ b/core/modules/views/src/Tests/Handler/FieldDateTest.php
@@ -121,13 +121,11 @@ public function testFieldDate() {
-    $test = $this->container->get('date.formatter')->formatInterval(REQUEST_TIME - $time, 2);
     $intervals = array(
-      'raw time ago' => $test,
-      'time ago' => t('%time ago', array('%time' => $test)),
-      'raw time span' => $test,
-      'inverse time span' => -$test,
-      'time span' => t('%time ago', array('%time' => $test)),
+      'raw time ago' => $this->container->get('date.formatter')->formatTimeDiffSince($time),
+      'time ago' => t('%time ago', array('%time' => $this->container->get('date.formatter')->formatTimeDiffSince($time))),
+      // @todo Add tests for raw time span and time span.
+      // Issue: https://www.drupal.org/node/2488844

See http://cgit.drupalcode.org/drupal/commit/?id=91504462cd0efb5d3bc3330441d...

This was the issue referenced in the @TODO. This coverage needs to be updated to use all the different intervals (raw time ago, time ago, raw time span, inverse time span and time span), but use formatTimeDiffSince() instead. And then the @TODO can be removed.

Furthermore, that patch added (a bit lower):
$formatted = $this->container->get('date.formatter')->formatInterval($time - REQUEST_TIME, 2);
That will need to be changed to use formatTimeUntil().

I'm not sure why DateTest is failing though.

Anonymous’s picture

Seems like I misread #147. user.module was automerged, what I would have expected. So we're good there.

Also

Yeah, we should wait until someone can do a pass for formatInterval() again.

What do you mean with that? Someone should look for all occurrences again?

jhodgdon’s picture

Issue summary: View changes

#151 looks like the list of To Dos. I think we only needed to look at the latest commits related to this.

But after applying the new patch, looking at remaining formatInterval() calls would still be a good idea for the reviewer, to make sure they're only the ones listed in comment #140. So... I added that list in #140 to the issue summary, and updated it in general to make it even more clear. Hopefully.

Anonymous’s picture

Status: Needs work » Needs review
FileSize
2.39 KB
50.61 KB

This should fix the test failures and fix the merge issue as shown in #151.

I searched for formatInterval and could only find uses that fit in the three categories that we can't fix.

jhodgdon’s picture

Status: Needs review » Needs work

Looking good! Mostly... :)

I also looked through remaining calls to formatInterval() and agree that this patch catches all of them that should be fixed.

But I'm still confused about a couple of the conversions in this patch, and/or think they should be different -- two to be exact:

  1. +++ b/core/modules/aggregator/src/Controller/AggregatorController.php
    @@ -127,8 +127,8 @@ public function adminOverview() {
    -      $row[] = ($last_checked && $refresh_rate ? $this->t('%time left', array('%time' => $this->dateFormatter->formatInterval($last_checked + $refresh_rate - REQUEST_TIME))) : $this->t('never'));
    ...
    +      $row[] = ($last_checked && $refresh_rate ? $this->t('@time left', array('@time' => $this->dateFormatter->formatTimeDiffSince($last_checked + $refresh_rate))) : $this->t('never'));
           $links['edit'] = [
    

    This one for Aggregator checking "time left" seems wrong. I think it should be Until and not Since, because it's formatting "how long until the next expected check". Right?

  2. +++ b/core/modules/system/system.tokens.inc
    @@ -68,7 +68,7 @@ function system_token_info() {
    -    'description' => t("A date in 'time-since' format. (%date)", array('%date' => \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - 360, 2))),
    +    'description' => t("A date in 'time-since' format. (%date)", array('%date' => \Drupal::service('date.formatter')->formatTimeDiffSince(REQUEST_TIME - 360))),
       );
    

    This one is still bothering me, every time I look at it, and I wonder if we should just leave it as formatInterval() but change the input to a more reasonable number of seconds? I think that was the intent, although the code is simply wrong to do REQUEST_TIME - 360 (which is formatting the time elapsed since some date in 1972 I guess, but maybe that was the actual intent?).

    So. The idea of this line is to show an example of what a "since" token for a date will look like, like "1 hour 30 minutes" or "1 year 2 months" or whatever. The original line here will be showing something like "30 years xxx". We decided in an earlier comment that they probably intended it to be "6 minutes" and got it wrong. Who knows, since there's not a code comment here. But in any case, I'm wondering if maybe we should just pick something relatively short like "1 hour 30 minutes" and leave this as formatInterval() to format the sample, to avoid REQUEST_TIME being introduced? So we would just leave this at formatInterval(3630) and add a code comment saying that this should output a sample of 1 hour 30 minutes.

    Thoughts?

Anonymous’s picture

Status: Needs work » Needs review

1. You are totally right. Fixed.

2. Hm. This indeed was wrong with the formatInterval() method. But I think the change is good as-is. We introduced the wrapper method for this usage. And all examples in the code around this line use the REQUEST_TIME parameter. As an alternative, we could also fix the date to get rid of the REQUEST_TIME parameter and comment that the output would be "similar to 1 year 1 month".
I don't think we have an ideal solution here, so I'm definitely willing to change it if you think it's better with formatInterval().

Anonymous’s picture

Got a "bad gateway" while posting, and didn't notice the patch was gone while posting again. So here it is!

jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

Fair enough. Let's leave that as is then.

I believe this is ready to go, and the issue summary is updated. Hopefully we can get it in this time before we need another big change. ;)

jhodgdon’s picture

FYI, another potential conflict with #2399211: Support all options from views fields in DateTime formatters. If that issue gets committed first, we'll need to look at its uses of formatInterval() and convert them as well. If this gets committed first, we should make a new patch for that other issue to convert its formatInterval() calls there.

jhodgdon’s picture

Another one to watch: #2502571: Date format granularity should only render adjacent units. This is changing how formatInterval() deals with cases that would currently output stuff like "1 year 1 second" to just do "1 year" until it gets to "1 year 1 month". We'll need to do the same for formatDiff() if that is accepted; if this gets in first we'll need to add formatDiff() to the patch on that issue.

mpdonadio’s picture

Major work on #2455125: Update EntityViewsData use of generic timestamp to use Field API formatter is also done, and is awaiting review. Same situation as described in #159

jhodgdon’s picture

Yes, we have a lot of race conditions here. ;)

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

add-2456521-156_0.patch no longer applies.

error: patch failed: core/modules/forum/forum.module:636
error: core/modules/forum/forum.module: patch does not apply

mpdonadio’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
50.6 KB
git checkout 1daf8de1b6e6ad3ffa14db187fb4d23f085643f2
git apply --index add-2456521-156_0.patch 
git rebase 8.0.x

Clean rebase; no manual merges.

jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

Looks like the bit after the patched line in forum.module was removed in some other patch, which is why this patch didn't apply. Checked over the reroll and it's all good. Thanks! Back to RTBC.

alexpott’s picture

Version: 8.0.x-dev » 7.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed 4ba73b4 and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation to the issue summary, the thorough test and the last minute additions.

  • alexpott committed 4ba73b4 on 8.0.x
    Issue #2456521 by pjonckiere, mpdonadio, jhodgdon, rteijeiro,...
mpdonadio’s picture

+++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php
@@ -214,12 +234,186 @@ public function getSampleDateFormats($langcode = NULL, $timestamp = NULL, $timez
+    foreach (array('y', 'm', 'd', 'h', 'i', 's') as $value) {
+      if ($interval->$value > 0) {
+        // Switch over the keys to call formatPlural() explicitly with literal
+        // strings for all different possibilities.
+        switch ($value) {
+          case 'y':
+            $interval_output = $this->formatPlural($interval->y, '1 year', '@count years', array(), array('langcode' => $options['langcode']));
+            break;
+
+          case 'm':
+            $interval_output = $this->formatPlural($interval->m, '1 month', '@count months', array(), array('langcode' => $options['langcode']));
+            break;
+
+          case 'd':
+            // \DateInterval doesn't support weeks, so we need to calculate them
+            // ourselves.
+            $interval_output = '';
+            $days = $interval->d;
+            if ($days >= 7) {
+              $weeks = floor($days / 7);
+              $interval_output .= $this->formatPlural($weeks, '1 week', '@count weeks', array(), array('langcode' => $options['langcode']));
+              $days -= $weeks * 7;
+              $granularity--;
+            }
+            if ($granularity > 0 && $days > 0) {
+              $interval_output .= ($interval_output ? ' ' : '') . $this->formatPlural($days, '1 day', '@count days', array(), array('langcode' => $options['langcode']));
+            }
+            break;
+
+          case 'h':
+            $interval_output = $this->formatPlural($interval->h, '1 hour', '@count hours', array(), array('langcode' => $options['langcode']));
+            break;
+
+          case 'i':
+            $interval_output = $this->formatPlural($interval->i, '1 minute', '@count minutes', array(), array('langcode' => $options['langcode']));
+            break;
+
+          case 's':
+            $interval_output = $this->formatPlural($interval->s, '1 second', '@count seconds', array(), array('langcode' => $options['langcode']));
+            break;
+
+        }
+        $output .= ($output ? ' ' : '') . $interval_output;
+        $granularity--;
+      }
+
+      if ($granularity == 0) {
+        break;
+      }
+    }
+

$granularity can change twice in the loop so the final if needs to be `$granularity <= 0`. Running into this bug right now. Will see if I can show it in the unit test.

mpdonadio’s picture

  • alexpott committed 4ba73b4 on 8.1.x
    Issue #2456521 by pjonckiere, mpdonadio, jhodgdon, rteijeiro,...
vijaycs85’s picture

Status: Patch (to be ported) » Needs review
FileSize
8.25 KB

Initial port of just new APIs. Will port tests and update instances to use new API.


Note to self:Add fallback to PHP < 5.3
Sharique’s picture

I've replaced few instances of format_interval with format_time_diff_since or format_time_diff, let me know if anybody any replacement is incorrect.

Status: Needs review » Needs work

The last submitted patch, 172: add-2456521-172.patch, failed testing.

Sharique’s picture

Status: Needs work » Needs review
FileSize
18.39 KB
9.18 KB

Reverted changes in tests.

jhodgdon’s picture

Just a note... Our current policy on backports says that this issue should be put back to 8.x and marked Fixed, and then a new follow-up issue should be opened for the 7.x port. So... someone should probably do that, and move the patches over there. Thanks!

Sharique’s picture

jhodgdon’s picture

Version: 7.x-dev » 8.1.x-dev
Status: Needs review » Fixed

Yes. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.