Carbon: Way to honour the CarbonInterval spec when using forHumans()?

Created on 25 Jul 2018  路  6Comments  路  Source: briannesbitt/Carbon

Hi,

Apologies if perhaps this is not the best place to ask this.

I've been using CarbonInterval::forHumans as a way to display human friendly descriptions of DateIntervals.

I'm now looking for a way to be able to either force or honour the interval string spec that is passed to CarbonInterval. Tried playing with cascade factors and macros to no avail, since there doesn't seem to be a way to retrieve the originaly passed spec string.

In short, the behaviour I'm looking for is as follows:

$cases = ['P3Y', 'P4W', 'P14D', 'P3M', 'P6W', 'P6M', 'P1D', 'P1Y', 'P2W', 'P7D', 'P2Y', 'P30D', 'P40D', 'P2M', 'P2W', 'P1M15D'];

foreach ($cases as $case) {
    $ci = CarbonInterval::make($case);
    $ci->setLocale('en');
    echo sprintf('%s -> %s', $case, $ci->forHumans());
}

Wanted result:

P3Y -> 3 years
P4W -> 4 weeks
P14D -> 14 days
P3M -> 3 months
P6W -> 6 weeks
P6M -> 6 months
P1D -> 1 day
P1Y -> 1 year
P2W -> 2 weeks
P7D -> 7 days
P2Y -> 2 years
P30D -> 30 days
P40D -> 40 days
P2M -> 2 months
P2W -> 2 weeks
P1M15D -> 1 month 15 days

Current result:

P3Y -> 3 years
P4W -> 4 weeks
P14D -> 2 weeks (wrong)
P3M -> 3 months
P6W -> 6 weeks
P6M -> 6 months
P1D -> 1 day
P1Y -> 1 year
P2W -> 2 weeks
P7D -> 1 week (wrong)
P2Y -> 2 years
P30D -> 4 weeks 2 days (wrong)
P40D -> 5 weeks 5 days (wrong)
P2M -> 2 months
P2W -> 2 weeks
P1M15D -> 1 month 2 weeks 1 day (wrong)

In other words, I want to display a verbatim human version of the spec string.
Is there a way to achieve this? If so, can someone point me in the right direction?

Much appreciated.

All 6 comments

The quick and dirty answer:

CarbonInterval::setCascadeFactors([
  'milliseconds' => [99999999, 'microseconds'],
  'seconds' => [99999999, 'milliseconds'],
  'minutes' => [99999999, 'seconds'],
  'hours' => [99999999, 'minutes'],
  'days' => [99999999, 'hours'],
  'weeks' => [99999999, 'days'],
  'months' => [99999999, 'weeks'],
  'years' => [99999999, 'months'],
]);

For the clean solution, I have to think of it a bit.

The main problem here is DateInterval does not store weeks, so we have no way to know if an interval has been created with 1W or 7D. For other units, it's fine, but to do the same with weeks, we would need to store the original string SPEC given (if created with SPEC string) or store an initial weeks count. But this one can be wrong after some operations (add/sub/times/cascade).

Thanks for the fast feedback, tried your solution and it _almost_ works :)
I guess the cases that do not work are related to the things you pointed out regarding how weeks are handled internally:

P3Y -> 3 years
* P4W -> 28 days
P14D -> 14 days
P3M -> 3 months
* P6W -> 42 days
P6M -> 6 months
P1D -> 1 day
P1Y -> 1 year
* P2W -> 14 days
P7D -> 7 days
P2Y -> 2 years
P30D -> 30 days
P40D -> 40 days
P2M -> 2 months
* P2W -> 14 days
P1M15D -> 1 month 15 days

I guess it should be possible to work around the week cases by handling them differently, but without access to the original spec string it would be necessary to build some kind of wrapper proxy class around Carbon.

Again, new DateInterval('P1W') and new DateInterval('P7D') return exactly the same object. SO the only way to handle it is to intercept the constructor, then in this interceptor, copy week count or original SPEC string. But we probably won't do this because intervals can be created in many other way than SPEC strings and as they are mutable, what we would store would become invalid after operations.

You need to parse the string to get week, something like:

if (preg_match('/[PDMY](\d+)W/', $spec, $match)) {
  echo 'There is '.$match[1].' weeks';
  // remove week * 7 days, create an interval dedicated to week displayed with the default cascade factors
}

@kylekatarnls yes, i understand that this doesn't make sense from the library's pov, all clear, thanks a lot for your support :+1:

To sum-up the answer: you can disable cascading except for weeks.

With weeks and days, you must choose between:

  1. Cascading weeks even when days spec given:
CarbonInterval::setCascadeFactors([
  'milliseconds' => [99999999, 'microseconds'],
  'seconds' => [99999999, 'milliseconds'],
  'minutes' => [99999999, 'seconds'],
  'hours' => [99999999, 'minutes'],
  'days' => [99999999, 'hours'],
  'weeks' => [7, 'days'],
  'months' => [99999999, 'weeks'],
  'years' => [99999999, 'months'],
]);
  1. Keep days even when week spec given:
CarbonInterval::setCascadeFactors([
  'milliseconds' => [99999999, 'microseconds'],
  'seconds' => [99999999, 'milliseconds'],
  'minutes' => [99999999, 'seconds'],
  'hours' => [99999999, 'minutes'],
  'days' => [99999999, 'hours'],
  'weeks' => [99999999, 'days'],
  'months' => [99999999, 'weeks'],
  'years' => [99999999, 'months'],
]);

But you can change the settings dynamically:

foreach ($cases as $case) {
  CarbonInterval::setCascadeFactors([
    'milliseconds' => [99999999, 'microseconds'],
    'seconds' => [99999999, 'milliseconds'],
    'minutes' => [99999999, 'seconds'],
    'hours' => [99999999, 'minutes'],
    'days' => [99999999, 'hours'],
    'weeks' => [preg_match('/[PDMY](\d+)W/', $case) ? 7 : 99999999, 'days'],
    'months' => [99999999, 'weeks'],
    'years' => [99999999, 'months'],
  ]);
  $ci = CarbonInterval::make($case);
  $ci->setLocale('en');
  echo sprintf('%s -> %s', $case, $ci->forHumans());
}

This will work as long as the spec string does not contain both weeks and more than 7 days.

Was this page helpful?
0 / 5 - 0 ratings