YYYY? yyyy!
Anastasiia Vorobeva
Posted on November 14, 2024
Do you know what's the difference between the 'Y' and 'y' characters in the Java date pattern? In this article, we'll explore how an incorrect date format can cause an error. We'll also introduce our new V6122 diagnostic rule for Java that'll save you from sudden time travel.
Introduction
After dusting off our big TODO notebook, we stumbled upon a particularly interesting case. A commenter on the article highlighted the potential issue.
The comment
By the way, here's an idea for you: SimpleDateFormat in Java can pull off a New Year's surprise—a year written in lowercase letters isn't the same as a year written in capital letters. In the last week of the year, you may suddenly find yourself in the future, as "YYYY" for the 27.12.2021–31.12.2021 dates is 2022, not 2021 as some might expect.
Let's get to the bottom of this.
Issue analysis
Date formatting
If you've somehow forgotten what SimpleDateFormat is, you can brush up on your knowledge here.
To store and display dates, we often need them to follow a particular pattern. SimpleDateFormat is a class that enables us to easily format a date according to a given pattern. In addition to formatting, SimpleDateFormat can also parse strings and convert them to the date object.
Is that all Java has to offer? In addition to SimpleDateFormat, the DateTimeFormatter class can also format dates.
Let me show you an example of using these two classes.
If we format a date via SimpleDateFormat:
public static void main(String[] args) {
Date date = new Date("2024/12/31");
var dateFormatter = new SimpleDateFormat("dd-MM-yyyy");
System.out.println(dateFormatter.format(date));
}
The console displays the following:
31-12-2024
What's happened here?
We have date, and we want to save/display it in a specific format. To achieve it, we create the SimpleDateFormat object by passing a pattern string to the constructor. The date is formatted exactly according to this pattern. The format method returns a string representation of the formatted date. This is exactly what we wanted.
Here's the same thing, but we use DateTimeFormatter:
public static void main(String[] args) {
LocalDate date = LocalDate.of(2024, 12, 31);
var formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
System.out.println(formatter.format(date));
}
The console displays the following:
31-12-2024
Same steps and result, but there's a slight difference: DateTimeFormatter enables us to format only the dates represented by the class implementing the TemporalAccessor interface. For example, these include LocalDate and LocalDateTime. SimpleDateFormat formats only objects of the Date class.
Let's get back to the comment. Does using 'Y' instead of 'y' in a date pattern really change the result?
Take a look at the code:
public static void main(String[] args) {
Date date = new Date("2024/12/31");
var dateFormatter = new SimpleDateFormat("dd-MM-YYYY");
System.out.println(dateFormatter.format(date));
}
The console displays the following:
31-12-2025
Oops. Is it the same when it comes to DateTimeFormatter?
Here's the code:
public static void main(String[] args) {
LocalDate date = LocalDate.of(2024, 12, 31);
var formatter = DateTimeFormatter.ofPattern("dd-MM-YYYY");
System.out.println(formatter.format(date));
}
The console displays the following:
31-12-2025
We did travel a year into the future. It's time to see what's going on.
Essence of issue
My first step was to look at the documentation for the SimpleDateFormat class. Below is a small fragment of the table that describes how the date pattern interprets the alphabetic characters we're interested in:
Letter | Date or Time Component | Presentation | Examples |
---|---|---|---|
y | Year | Year | 1996; 96 |
Y | Week year | Year | 2009; 09 |
A week year? Let me explain.
The week year is a year based on the week number of the year. What does it mean and why is it important?
In certain tasks, the sequence number of the week in the year is important. To determine the sequence number of the week, we need to decide which week to consider first. This is because the transition from one year to another often results in some weeks straddling both years. So, how can we tell which year this week belongs to? The ISO-8601 standard regulates this.
According to this standard, the first week of the year must meet the following conditions:
- the first day of the week is Monday;
- the minimum number of days of the year in a week is four.
From this, we can deduce a simple rule: a week is considered to be the first of the year if Thursday falls in January.
A real-life example can clarify this further.
Let's take the date from the example, which is 31.12.2024. Below is the calendar snippet for the week that includes our date (December 2024–January 2025):
Mo | Tu | We | Th | Fr | Sa | Su |
---|---|---|---|---|---|---|
30 | 31 | 1 | 2 | 3 | 4 | 5 |
This week is considered the first week of 2025 because it satisfies the above conditions (it has five January days). So, if we use the 'Y' specifier, we get the 2025 year.
As you can guess, the case with DateTimeFormatter is exactly the same.
It's important to note that we can travel not only to the future but also to the past.
Let's take another date, 01.01.2027.
Will the first week of 2027 include January 1? Again, let's consult the calendar.
Below is the calendar snippet for the week that includes our date (December 2026–January 2027):
Mo | Tu | We | Th | Fr | Sa | Su |
---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
As there are only three January days in this week (and it should be four in the first week according to the requirements), it counts as the last week of 2026. So, the date, when formatted using the 'Y' character, will show us 2026.
Here's the evidence. Take a look at the code:
public static void main(String[] args) {
LocalDate date = LocalDate.of(2027, 1, 1);
var formatter = DateTimeFormatter.ofPattern("dd-MM-YYYY");
System.out.println(formatter.format(date));
}
The console displays the following:
01-01-2026
Okay, we've dissected the issue. We also found it interesting enough to add a dedicated diagnostic rule to PVS-Studio Java analyzer.
Errors in real projects
The diagnostic rule is written, so it's time to test it.
During the development of the analyzer, one of the testing stages involves running regression tests. We have an article about this process. In short, when we add a new diagnostic rule, we analyze a large pool of open-source projects and compare the new reports with the reference ones.
In the case of this diagnostic rule, the analyzer issued new warnings for several projects. Let's take a look at them.
Bouncy Castle
In this project, the code fragments that the diagnostic rule points to are identical, so I'll show only one of them.
Take a look at the code:
public Builder setPersonalisation(Date date, .... {
....
final OutputStreamWriter
out = new OutputStreamWriter(bout, "UTF-8");
final DateFormat
format = new SimpleDateFormat("YYYYMMdd"); // <=
out.write(format.format(date));
....
}
The PVS-Studio warning:
V6122 Usage of 'Y' (week year) pattern was detected: it was probably intended to use 'y' (year). SkeinParameters.java 246
Firstly, I took a look at GitHub. What if it really was a bug, the developers had already found it and committed a fix? That's exactly what happened. Here's a link to the commit, you can check it out. All of our 'Y' (week year) characters in the pattern were replaced with 'y' (year).
You may wonder why the commits were made a relatively long time ago. Let me explain. Our regression tests aren't meant to continuously control the quality of this or that open-source project. The task is to see how the report changes when new diagnostic rules are added: no old warnings should disappear, no new errors should appear, as that would indicate that the analyzer has an issue. So, the checked code must be the same.
Opengrok
Now let's take a look at the second project where the diagnostic rule got triggered.
The PVS-Studio warning:
V6122 Usage of 'Y' (week year) pattern was detected: it was probably intended to use 'y' (year). RepositoryInfo.java 77
The code:
public class RepositoryInfo implements Serializable {
....
protected static final SimpleDateFormat
outputDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm Z");
....
}
Just as with the previous project, I rushed to the commits to see what was going on. First of all, it's worth noting that the field moved to its derived class, Repository, as a result of refactoring (here's the link to the commit). I searched further and found a commit containing the fix. The 'Y' character in the date pattern has been replaced with 'y':
public abstract class Repository extends RepositoryInfo {
....
protected static final SimpleDateFormat
OUTPUT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm Z");
....
}
So, it was an error here as well.
Conclusion
At PVS-Studio, we welcome any feedback from the community. After addressing the comment from a Habr user, we've enhanced the Java analyzer by adding some useful diagnostic rules. So, if you have any thoughts you'd like to share with us, we'd love to chat with you in the comment section of this article.
By the way, the diagnostic rule has been introduced in the October 7.33 release. So, if you want to try our analyzer, use this link.
That's all. Let's wrap things up here. Hopefully, the sudden trip a year forward (or backward) won't catch you off guard.
Posted on November 14, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024