Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

createFromFormat throw an exception for a format from which hasFormat returned true #2831

Open
Tristan-MyAnaPro opened this issue Aug 8, 2023 · 7 comments

Comments

@Tristan-MyAnaPro
Copy link

Hello,

I encountered an issue with the following code:

Carbon::hasFormat('20223-08-03', 'Y-m-d'); // return `true`
Carbon::createFromFormat('Y-m-d', '20223-08-03'); // Throw an exception

Carbon version: 2.68.1

PHP version: 8.1.21

I expected to get:

A carbon instance from createFromFormat

OR

false from hasFormat.

But I actually get:

hasFormat return true for the Y format "flag" with a 5 digits year but createFromFormat throw an exception (when in strict mode) for the same flag with a 5 digits year (eg. "22002").


It happens because of the regex in src/Carbon/Traits/Options.php:84 is ([1-9]?[0-9]{4}) which allow 5 characters.

Either hasFormat should accept only 4 digits for the Y flag or createFromFormat should support 5 digits year, although I'm not sure this is possible as createFromFormat use the base function from the Datetime class ?

Or I missed something in the doc about this case (which is totally possible ^^)

Thanks for your help!

@Tristan-MyAnaPro Tristan-MyAnaPro changed the title createFromFormat throw a exception for a format hasFormat returned true createFromFormat throw an exception for a format from which hasFormat returned true Aug 8, 2023
@kylekatarnls
Copy link
Collaborator

That's a pity that DateTime::createFromFormat('Y-m-d', '20223-08-03') return false since it's capable to build new DateTime('20223-08-03') and calling ->format('Y-m-d') on this last object just output "20223-08-03".

It's a bit unfortunate IMO that PHP have a constraint on the number of digits in createFromFormat but is still able to parse, create, and display datetime with a 5-digits year :(

Documentation of DateTime::createFromFormat refers to DateTime::format for formats and 4 digits is mentioned explicitly so let's aligne on 4 digits for Carbon::hasFormat.

Y    A full numeric representation of a year, at least 4 digits, with - for years BCE.

@kylekatarnls kylekatarnls added this to the 2.70.0 milestone Aug 9, 2023
@kylekatarnls
Copy link
Collaborator

kylekatarnls commented Aug 9, 2023

Funny enough, > 4 digits does not work but less is OK: DateTime::createFromFormat('Y-m-d', '1-08-03')->format('Y') output 0001

kylekatarnls added a commit to kylekatarnls/Carbon that referenced this issue Aug 9, 2023
kylekatarnls added a commit to kylekatarnls/Carbon that referenced this issue Aug 9, 2023
@kylekatarnls
Copy link
Collaborator

Side note, meanwhile you can use:

Carbon::canBeCreatedFromFormat('20223-08-03', 'Y-m-d')

Or use a try-catch.

@Tristan-MyAnaPro
Copy link
Author

Tristan-MyAnaPro commented Aug 10, 2023

Documentation of DateTime::createFromFormat refers to DateTime::format for formats and 4 digits is mentioned explicitly so let's aligne on 4 digits for Carbon::hasFormat.

Y    A full numeric representation of a year, at least 4 digits, with - for years BCE.

Well, at least 4 digits mean it should be minimum 4 digits long but allow for more. The PHP doc for Datetime::format even have an exemple with a 5 digit year 🤣 Examples: -0055, 0787, 1999, 2003, 10191

Edit : ohh I check on the wrong page, format support N digits long years but createFromFormat, explicitly restrict to Y : [...], up to 4 digits

Anyway thanks for the consideration on aligning hasFormat with what createFormFormat support !

Side note, meanwhile you can use:

Carbon::canBeCreatedFromFormat('20223-08-03', 'Y-m-d')
Or use a try-catch.

I've fixed my code with a try catch before making this issue but I might use canBeCreatedFromFormat in the futur as I find it quite elegant and it offer a consistent return value independent of strict mode, thx for the tip !

@Tristan-MyAnaPro
Copy link
Author

Tristan-MyAnaPro commented Aug 10, 2023

Okay, this might seems weird but I'd like you to reconsider "fixing" hasformat for the following reasons:

  1. Fixing hasformat in 2.70.0 is a breaking change and some people requiring carbon ^2.0 relying on the fact that hasFormat accept 5 digits years might encounter issues.
  2. The PHP doc on createFromFormat explicitly forbid more than 4 digits years and carbon already give a way to validate this rule with canBeCreatedFromFormat().
    • I should have used canBeCreatedFromFormat() from the beginning, the Carbon doc even says:
    • You can test if a date matches a format for createFromFormat() [...] Carbon::canBeCreatedFromFormat() which also ensure data is actually enough to create an instance.

    • I'm blind...
  3. hasFormat is only mentioned in the Carbon doc on the topic of formatting dates, not creating them. So hasFormat should only be used to check if a date can be formatted using this format, not created from this format.
    • // hasFormat() does not interpret modifiers, it checks strictly if ->format() could have produce the given string with the given format
      // The reverse hasFormat method allows you to test if a string looks like a given format

The real "fix" should be to align hasFormat regex with the native Datetime::format(), allowing more than 5 digits year.
This might be a breaking change too and should be discussed or even pushed back to 3.0.

What's your pov ?

@kylekatarnls
Copy link
Collaborator

Indeed changing hasFormat is messy, I stopped at this point yesterday because tests (which are the backward-compatibility promise) failed on #2832.

So we already make a commitment for 2.x on what hasFormat('Y') should respond.

I see that PHP 8.2 introduced X format which is allowing more than 4 digits. I missed that. I think 3.x should be at least as strict as PHP and so the X format would be the solution for those needing more digits.

Else, yeah ::hasFormat() should reflect ->format() and we could ignore completely to try to make it work with ::createFormFormat() as both formats are not compatible, it can only lead to mistake.

But even ::canBeCreatedFromFormat() is not super great. It's actually try-catch under the hood because it's basically the only way to be sure. So if you use it then call ::createFormFormat() later, then DateTime::createFormFormat() is called twice, if the operation is made in a loop (reading line-by-line a CSV file for instance), then performance would matter.

A ::tryFormFormat() (similar to enum tryFrom) could be more performant as getting silent errors as null which can be checked easily but the actual value that can be re-used immediately in case of success.

@kylekatarnls kylekatarnls removed this from the 2.70.0 milestone Aug 10, 2023
@neromantic
Copy link

Fun fact

If you wrap ::createFormFormat() within a try-catch block, handling InvalidFormatException, PHPStan will identify it as a dead catch.

@kylekatarnls kylekatarnls added this to the Future milestone Jan 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants