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
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. |
Comment | File | Size | Author |
---|---|---|---|
#174 | 2456521-d7-171-174-interdiff.txt | 9.18 KB | Sharique |
#174 | add-2456521-174.patch | 18.39 KB | Sharique |
#172 | add-2456521-172.patch | 19.82 KB | Sharique |
#171 | 2456521-d7-171.patch | 8.25 KB | vijaycs85 |
#154 | inter-147-154.txt | 2.39 KB | Anonymous (not verified) |
Comments
Comment #1
jhodgdonThe 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.
Comment #2
jhodgdonLet's at least fix the docs so they warn people about this problem.
Comment #3
jhodgdonI 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.
Comment #4
yched CreditAttribution: yched commentedThis 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 ? ;-)
Comment #5
jhodgdonIt'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.
Comment #6
larowlan14 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.
Comment #7
mpdonadioIs 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.
Comment #8
jhodgdonCorrect 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.
Comment #9
rteijeiro CreditAttribution: rteijeiro commentedFixed comments that fit in 80 chars lines.
Comment #10
Anonymous (not verified) CreditAttribution: Anonymous commentedLet'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.
Comment #12
jhodgdonUmmm... 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.
Comment #13
Anonymous (not verified) CreditAttribution: Anonymous commentedYes, 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.
Comment #14
jhodgdonOK. 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?
Comment #15
Anonymous (not verified) CreditAttribution: Anonymous commentedSeems 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?
Comment #16
jhodgdonThe 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.
Comment #17
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedPosting 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
Comment #19
jhodgdonRegarding #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?
Comment #20
mpdonadioAre 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.
Comment #21
jhodgdonThese 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.
Comment #22
Anonymous (not verified) CreditAttribution: Anonymous at XIO commented@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)
Comment #23
jhodgdonSo right now, formatInterval is doing:
What it needs to be doing is something like this for $units:
and then in that $output line, something like:
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.
Comment #24
jhodgdonOh 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.
Comment #25
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedkludges like this
Wow.
How about changing the prefix/suffix attributes it to an $attributes array instead?
That way we can extend it more easily later on. Maybe we could also add the optional $timezone, $granularity and $langcode there as well?
Comment #26
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedIn 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.
Comment #27
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedLocally fixed the extra spaces before testFormatInterval(), so no need to highlight that :)
Comment #28
jhodgdonAttributes: 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:
I would avoid the word "larger" here, talking about times, and instead use either "before" or "after".
Slight optimization: could just create the TimeZone object once?
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?
I think we can just call $interval->$key rather than asking $interval to format itself? or use $value?
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)?
At some point, some tests with time zones seem like a good idea.
Comment #29
jhodgdonWhoops. 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!
Comment #30
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedThank 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.
Comment #31
tim.plunkett#109878: format_interval should support leap-year calculation is a much much older issue, but this issue seems to have more momentum.
Comment #32
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedI'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.
Comment #33
rteijeiro CreditAttribution: rteijeiro commentedAddressed 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.
Comment #35
mpdonadioThink this patch needs to be adjusted for #2459155: Remove REQUEST_TIME from bootstrap.php.
Not sure why a time zone is needed here, when the interval is being calculated off of two timestamps from the same timezone.
Comment #36
mpdonadioThe docblock here should have a @covers instead of a @see.
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.
Comment #37
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedThanks 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.
Comment #38
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedI'll take a look at the timezone issue.
Comment #39
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedI'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
Comment #40
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedSome 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
Comment #41
jhodgdonThanks 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!
Should say "The following keys..."
Formatting here: The text on the second line below the - list bullets should be indented 2 spaces.
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.
What does this mean?
Would this be easier if we did something like
in the switch statement, and then just one
Why are we still using formatInterval for these cases?
Comment #42
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedLet'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"
Comment #43
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedTriggering testbot.
Comment #46
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedFixed some tests. Let's see what's left.
Comment #47
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedAdded support for when there is no time difference. I'll read the full issue again, but I think we've got everything covered now.
Comment #48
mpdonadioNeeds to use @covers instead of @see. Otherwise, I think this looks good. I will do a full review of this tonight.
Comment #49
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedIn #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.
Comment #50
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedFound how to do the @covers thing in the phpunit documentation.
Also fixing the test failure I introduced in the last patch.
Comment #51
mpdonadioWhen an annotation fails, look for other uses in core.
With this change, I get the same single failure as #47:
Comment #52
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedAh yes, I should have thought of that :)
Comment #54
mpdonadioYou 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.
Comment #55
mpdonadioNeeds space between if and opening paren.
Same, double check other places. See https://www.drupal.org/coding-standards#controlstruct
Does this mean that the prefix and suffix can't be translated? Not sure what is best here.
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.
Comment #56
mpdonadioOK 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.
Comment #57
jhodgdonRE #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...
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.
Second line here needs 2 spaces of indentation. Same in the next bullet point.
So we need to say "translated string"
These should also say the default is empty?
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?
Same here.
Here, it seems like it should just be formatDiff($interval) shouldn't it?
Might just pass in NULL here for second arg?
Same here. If we have this NULL capability, let's use it?
What is this 1 as second arg here? Seems wrong?
The rest of the changes have similar questions/issues. Am I misunderstanding something?
Comment #58
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedComment #59
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedThank 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.
Comment #60
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedI 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.
Comment #61
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedComment #62
jhodgdonThis is getting closer!
I think there are still a couple of problems:
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?
Do we really need this? ;)
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.
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 -.
Maybe the correct way to call this would be formatDiff(0, $interval)?
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?
This section of code seems to be doing the right thing with t() and the prefix/suffix, in contrast to most of the rest.
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?]
This doesn't look right still, see previous review.
Comment #63
Anonymous (not verified) CreditAttribution: Anonymous at XIO commented- 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.
Comment #64
jhodgdonSee 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...
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....
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...
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.
Comment #65
David_Rothstein CreditAttribution: David_Rothstein commentedThis 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?:
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.
Comment #66
David_Rothstein CreditAttribution: David_Rothstein commentedI think this could be backported to Drupal 7 too.
Comment #67
David_Rothstein CreditAttribution: David_Rothstein commentedI 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.
Comment #68
David_Rothstein CreditAttribution: David_Rothstein commentedComment #69
jhodgdonGood 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.
Comment #70
jhodgdonYeah, 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.
Comment #71
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedI'll have a look at all of that.
Comment #72
Anonymous (not verified) CreditAttribution: Anonymous at XIO commented#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.
Comment #74
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedFixing the test failures.
Comment #75
David_Rothstein CreditAttribution: David_Rothstein commentedI looked over the changes to add support for weeks, and those look good to me - nice.
Comment #76
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedOk, good! Thanks for reviewing.
Just removing a redundant comment here.
Comment #77
jhodgdonThis is very close! A few nitpicks and a few substantive comments... and can I just say nice work and very thorough testing?
Nitpick: boolean => Boolean
I would say $from and $to rather than just "from" and "to" here?
I find code with % to be a bit difficult to read... This could be written a bit differently:
I'm not strongly suggesting this though. Matter of preference. ;)
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. ;)
This may be a case where we should use the 'strict' option?
This may be another case where we should use the 'strict' option?
here too?
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'?
Same here?
And here?
Comment #78
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedYeah, that's better. If only we could get rid of the magic number 7 as well.
:D
That makes sense.
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.
Comment #79
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedForgot a #77.2 occurence.
Comment #80
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedOops.
Comment #81
jhodgdonI 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():
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.
The first formatDiff() still needs a granularity option. It only got added to the second one.
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?
Same here as in raw time hence?
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?
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?
Comment #82
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedRe #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.
Comment #83
jhodgdonI looked through this latest patch... I think it's all correct now. Phew! Thanks for all the hard work!
Comment #84
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedWow! Great! Let me find my party hat :D
Thanks for all the reviews!
I added a beta evaluation to the summary.
Comment #85
xjmThanks for the beta evaluation! The documentation in the patch looks great, as does the unit test coverage. Three questions, though:
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.
Comment #86
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedThe 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.
Comment #87
dawehnerOnly? 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.
Comment #89
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedExactly. 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.
Comment #90
jhodgdonI am updating the issue summary to address the points in #85. I think this should still be RTBC.
Comment #91
xjmAgain, if
formatInterval()
is so broken, why are we keeping it? From the summary (thanks @jhodgdon):But the patch doesn't reflect this. At least need to add some documentation to
formatInterval()
comparing how the two functions work and explaining whenformatInterval()
should not be used.Comment #92
jhodgdonThe patch does, in fact, add documentation to formatInterval:
Really, what more do you want??????
Comment #93
jhodgdonAs 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.
Comment #94
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedPerhaps 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.
Comment #95
jhodgdonOK, 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".
Comment #96
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedAlso, 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...
Comment #97
jhodgdonOK. 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?
Comment #98
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedI could review it, sure.
Comment #99
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedDuring 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.
Comment #100
jhodgdonOK I'll take it on.
Comment #101
jhodgdonWow, 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!).
Comment #103
jhodgdonTest 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.
Comment #104
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedI reviewed the interdiff, plus the whole patch. Found some issues:
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).
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.
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 :)
Not sure this is a good change. I would leave the old code as is, personally.
This does not look right...
Was changing $custom_format to $this->options['granularity'] really intentional here? (None of the other nearby code changed.)
This does not look right.... perhaps this is a case that needs to stay at formatInterval?
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.
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:
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:
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.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?
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:
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().
Comment #105
jhodgdonWow, 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.
Comment #106
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedI 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.Comment #108
mpdonadioReroll w/ manual merge in CommentTokenReplaceTest to choose the hunk from the patch.
Comment #109
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedThe manual merge is incorrect - it reverts the new "getChangedTimeAcrossTranslations" code:
Comment #110
mpdonadioGood 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.
Comment #111
jhodgdonThanks! 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.
Comment #112
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedAgreed, #110 looks good now.
Copying my comment from above, for easy access:
Comment #113
David_Rothstein CreditAttribution: David_Rothstein as a volunteer commentedOops :)
Comment #114
xjmYeah 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 useformatInterval()
instead for a less accurate result? (This is less important I guess sinceformatInterval()
has the details and they both reference each other).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()
andformatDiff()
less clear -- because ifformatDiff()
requires two arguments (which is the only reason you'd useformatInterval()
), 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 ofNULL
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()
andformatDiffSince()
. OrformatUntil()
andformatSince()
. Um, hopefully someone else could suggest better names. but it would be like: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()
andformatDiff()
were the right names, but they are parallel to the PHP method names, so that probably makes sense.Comment #115
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedGreat progress!
Re #112:
and re #114:
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.
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.
Comment #116
xjmSorry, I meant that the wrappers would also receive these options. I'll update my snippet to make that clear.
Comment #117
xjmThanks @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: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?
Comment #118
mpdonadioI 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.
Comment #119
mpdonadioOK, 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.
Comment #120
alexpottWhy 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.
Comment #121
mpdonadioOK, 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.
Comment #123
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedRe #117:
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.
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.
Comment #124
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedThis 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?
Comment #125
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedComment #126
mpdonadioUnit 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 properlygets set properly from the request object.Comment #127
jhodgdonI'm not really excited about this new direction but OK. Please someone update the summary.
Comment #129
mpdonadio@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.
Comment #130
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedI 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
Comment #132
jhodgdonIgnore 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.
Comment #133
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedPosting 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.
Comment #134
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedAll of the above except for the CR. Do we need a new one or can we update one?
Comment #135
mpdonadioSince 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.
Comment #136
jhodgdonI 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:
This is very clear. But...
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.
We should probably have @see links between all three of the formatDiff functions?
I love how much more readable this is. ;)))))
Remind me why we put strict=FALSE here? Seems like that's not right?
Comment #137
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedRe #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.
Comment #138
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedRe #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!
Comment #139
jhodgdonNote: #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]
Comment #140
jhodgdonGreat -- 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:
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!
Comment #141
XanoIf NULL is allowed, this should be
@param string|null $langcode
.Same here.
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.
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.
Comment #142
Anonymous (not verified) CreditAttribution: Anonymous at XIO commented@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?
Comment #143
jhodgdonThanks! I believe that Xano's comments in #141 have been addressed, so setting back to RTBC. Xano: feel free to disagree. ;)
Comment #145
mpdonadioPatch no longer applies.
Comment #146
jhodgdonRight, #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.
Comment #147
mpdonadio(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:
Not the easiest hand merge, but I think it is correct. Let's see what the Mr TestBot says.
Comment #148
jhodgdonDo 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?
Comment #149
mpdonadioYeah, we should wait until someone can do a pass for formatInterval() again. TestBot is also being slow tonight, too...
Comment #151
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedBased 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
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.
Comment #152
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedSeems like I misread #147. user.module was automerged, what I would have expected. So we're good there.
Also
What do you mean with that? Someone should look for all occurrences again?
Comment #153
jhodgdon#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.
Comment #154
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedThis 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.
Comment #155
jhodgdonLooking 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:
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?
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?
Comment #156
Anonymous (not verified) CreditAttribution: Anonymous at XIO commented1. 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().
Comment #157
Anonymous (not verified) CreditAttribution: Anonymous at XIO commentedGot a "bad gateway" while posting, and didn't notice the patch was gone while posting again. So here it is!
Comment #158
jhodgdonFair 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. ;)
Comment #159
jhodgdonFYI, 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.
Comment #160
jhodgdonAnother 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.
Comment #161
mpdonadioMajor 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
Comment #162
jhodgdonYes, we have a lot of race conditions here. ;)
Comment #163
alexpottadd-2456521-156_0.patch no longer applies.
Comment #164
mpdonadioClean rebase; no manual merges.
Comment #165
jhodgdonLooks 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.
Comment #166
alexpottCommitted 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.
Comment #168
mpdonadio$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.
Comment #169
mpdonadioFollowup w/ test and patch at #2504211: DateFormatter::formatDiff() ignore granularity in some circumstances..
Comment #171
vijaycs85Initial port of just new APIs. Will port tests and update instances to use new API.
Note to self:Add fallback to PHP < 5.3
Comment #172
Sharique CreditAttribution: Sharique as a volunteer commentedI'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.
Comment #174
Sharique CreditAttribution: Sharique as a volunteer commentedReverted changes in tests.
Comment #175
jhodgdonJust 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!
Comment #176
Sharique CreditAttribution: Sharique as a volunteer commentedCreate separate D7 port issue #2744597: Add format_time_diff() as a non-buggy alternative to format_interval() when the start and end of the interval are known. Can we close this issue now?
Comment #177
jhodgdonYes. Thanks!