And it is not even thread safe. We found this out because we had some flaky e2e test that were run concurrently and only failed every few days. The parsed date was just a few hours away from the expected so at first we thought it had to be something wrong with timezones or NTP.
Me too! It's crazy to me that I've never met you or the parent poster but we were living very similar lives the last week of the year due to the same Java DateTime design decision!
I understand that "week year" is basically a payroll creation, but I am a bit concerned about the last few days of 2021 were logged as 2022, and Jan 1st 2022 was even logged as 2023.
The last few days of 2021 logged as 2022 makes sense for payroll, but how the heck is Jan 1st 2022 put in the 2023 year?
This makes perfect sense if your role is in bookkeeping / accounting / finance. Your life is based on pay periods and not on day-of-year/month. A particular pay day "belonging" to a particular year is a meaningless affectation: people need to get paid every two weeks come hell or high water, and anything that can complicate the smooth running of that process is discarded.
As for the algorithm:
> The ISO 8601 definition for week 01 is the week with the first Thursday of the Gregorian year (i.e. of January) in it. The following definitions based on properties of this week are mutually equivalent, since the ISO week starts with Monday: […]
> This makes perfect sense if your role is in bookkeeping / accounting / finance. Your life is based on pay periods and not on day-of-year/month. A particular pay day "belonging" to a particular year is a meaningless affectation: people need to get paid every two weeks come hell or high water, and anything that can complicate the smooth running of that process is discarded.
It's not just payrolls, it's also things like deliveries or synchronisation. And saying "monday week 15" (or writing "W15 mon") aside from being short and precise can give a better ground for synchronisation or collision than "April 11th" e.g. the person you're talking to might know that every monday would be an issue and be able to say that that's not an option before even looking it up.
> I understand that "week year" is basically a payroll creation
It's not, it's a fiscal, or more generally business creation, allowing businesses to express everything in weekly basis, with no variance (all weeks are 7 days mon-sun with no fractionals, and every week belongs to a single year with no ambiguity).
This is very, very common usage in european businesses, and even casual settings, to the extent that you'll have a hard time finding a physical calendar without weeks printed on it.
It really is its own little DSL, and often subtly different yet similar enough across various languages. Compare also with printf and regular expressions.
Is it just me for thinking that using format strings instead of some strongly typed interface with verbose names for this is not great?
I would take `format(year(), '/', month(), '/', day())` over ad-hoc format strings by various APIs.
Reading the docs further this also stands out:
> For parsing with the abbreviated year pattern ("y" or "yy"), SimpleDateFormat must interpret the abbreviated year relative to some century. It does this by adjusting dates to be within 80 years before and 20 years after the time the SimpleDateFormat instance is created. For example, using a pattern of "MM/dd/yy" and a SimpleDateFormat instance created on Jan 1, 1997, the string "01/11/12" would be interpreted as Jan 11, 2012 while the string "05/04/64" would be interpreted as May 4, 1964. During parsing, only strings consisting of exactly two digits, as defined by Character.isDigit(char), will be parsed into the default century. Any other numeric string, such as a one digit string, a three or more digit string, or a two digit string that isn't all digits (for example, "-1"), is interpreted literally. So "01/02/3" or "01/02/003" are parsed, using the same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is parsed as Jan 2, 4 BC.
I hope nobody uses this to parse historical data that happens to become older than 80 years recently.
This is easier to read but not descriptive enough since there are different kinds year, month, day. Year can be 2 digits, 4 digits, regular or week year. Month can have leading zero or not, long name, short, name. Day could be Julian, leading zero or not, day of the week, etc
That's when you can be more precise if you want: `[year padding:zero repr:full base:calendar sign:automatic]`. The format is also checked on compile time.
+1 for strongly typed and compile-time checked, but I'm still not a fan of a sub-language within a string literal. The language already has syntax to structure things, why make a separate language within strings?
I don't know about Rust. Several people responded that Java already has a builder interface for creating a format string. That is possibly more type erased than the Rust macro equivalent. In C++ I would use variadic templates, as I originally proposed in my root comment: `format(year(), '/', ...)`.
The format_description! macro uses the same syntax as the format_description::parse method. If you want to support user-provided formatting strings (which you do), then you need such a method already, and once you’ve got that, why do the macro differently?
That components have parameters makes the non-literal approach even less compelling: take this which can produce the likes of “2:34:56pm”:
You could easily provide a prettier DSL so that you could write something like this:
use time::format_description::shorthand::{HOUR, MINUTE, SECOND, PERIOD, literal};
[
HOUR.padding_none().repr_12(),
literal(":"),
MINUTE,
":".into(), // even this if you wanted
SECOND,
PERIOD.case_lower(),
]
This wouldn’t be awful in the absence of the parse method, but really, once you have that, the format_description macro is just what you want: compact, checked at compile time, and matching a runtime equivalent which can take user-provided format strings.
(Now there are two or three changes I’d prefer to make to format_description’s syntax: I’d use = instead of :, the two being generally very similar but : far more regularly occurring in literal parts, so that the different = would make it scan better; and I think that escaping opening square brackets by doubling them but not requiring doubling for closing square brackets was a particularly bad idea; and I’m mildly inclined to prefer {} to []. So I might end up with "{hour padding=none repr=12}:{minute}:{second}{period case=lower}".)
In the context of the original article addressing Java 8, in the java.time.format package, there's a DateTimeFormatterBuilder that removes the sub-language element of the issue. With those builder methods, you can construct the fields in the order you want, with whatever precision, with padding, etc.
> Is it just me for thinking that using format strings instead of some strongly typed interface with verbose names for this is not great?
Yes, but it would not have helped with the bug in question. The parallel bug for a strongly typed interface would be "year()" returning this monstrosity, while "iso_year()" or some other poorly named variant returning the expected year.
No API is immune to footguns and bad design decisions.
It's harder to make that bug. The common case is "year of era", so it is likely to be used for "year()".
On the other hand the much less often used "year of week" would be named "year_of_week()" and hence it is clear to everyone that's not likely what you want.
> It's harder to make that bug. The common case is "year of era", so it is likely to be used for "year()".
That would be the sane thing to do. But the same applies to `YYYY`: it should be used for "year of era". But it hasn't been and that's the problem here. For contrast in moment.js, YYYY and yyyy do what you expect and "week year" is GGGG or gggg.
I forgot how bad JS's date API was until I tried getting the year out of a date.
var date = new Date()
date.getYear() // 122
I plugged this into another object and couldn't understand why it was setting the date to 0122. Like, what significance does 122 years from 1900 even have?
Turns out I had to use getFullYear, which is of course perfectly intuitive.
Well yes, as it's an extention of code written in the 1970s, when dates were most commonly referred to with 2 digits.
Even when javascript was written (and likewise with things like perl and php - and of course the underlying function dated back to the 70s ), it was still quite common for people to write dates by hand as 6/6/96 for June 6th 1996, hence people would often write something like
print day() + "/" + mon() + "/" + year()
If you wanted to put a 4 digit year many people
print day() + "/" + mon() + "/19" + year()
After 2000 came round, many sites would claim the year was 19100, I still saw these sites until well into the late 00s
However some people wrote print day() + "/" + mon() + "/" + (1900 + year())
Which would give the correct display.
Changing year() from returning a 2 digit (which in the 80s and even in the 90s was often preferable) to 4 digits would be a breaking change. Changing it to being "the last two digits" rather than "number of years since 1900" would be a breaking change. That's not something that people like to do, hence things like "getFullYear" to return a 4 digit year (or 3 digit for <1000 etc)
To be fair getYear has been deprecated for over 20 years (a bit after it was deprecated in Java for the same reason). The Y2K problem was a real thing in lots of places.
Why, if everything else in JS is 0 indexed? Isn't consistency a good thing in a language? If they'd gone the other way, wouldn't there be people here complaining that 1 indexing is whack?
Well because days are returned 1-indexed and calendars in general are 1-indexed, but of course the internal 'getMonth' method isn't meant to be a calendar, it's meant to provide an array index to ["January", "February", ...] as an interface to the other methods of printing a human-readable form.
The JS Date API was cribbed wholesale from Java's java.util.Date, which was cribbed wholesale from POSIX (and thus Unix) date APIs, which was designed in the 1970s. But both of the cribbings preferred compatibility with the older API rather than fixing obvious warts in the API like "year is since 1900, not 1 BC" or "month is 0-based instead of 1-based."
Sure, the issue is somewhat orthogonal. But I assume they wanted to keep the format string parsing minimal, hence the single letter format specifiers. Once you have a strongly typed API, you are no longer bound by this and you can have sensible names.
> The parallel bug for a strongly typed interface would be "year()" returning this monstrosity, while "iso_year()" or some other poorly named variant returning the expected year.
No, the parallel for this would be called `week_year()`, or `iso_week_year()`, to be paired with an `iso_week()` (or `year_week()` since there really is no other formal week-year).
The point is there's nothing about YYYY vs yyyy telling you at a glance that there is a significant semantic difference in the result.
Imagine an API which had methods 'getYear()' and 'GET_YEAR()' where the latter returns the payroll week year, while the former returns the, you know, actual year.
That's what having YYYY produce the week year feels like - a blatant violation of the principle of least surprise.
Which is why your op was saying this is like having an API where year() returns a surprising number thta isn't the year.
> The point is there's nothing about YYYY vs yyyy telling you at a glance that there is a significant semantic difference in the result.
There's the part that the entire LDML datetime grammar works like that.
Literally the next letter in your pattern will make it clear: M is the month, and m is the minute.
Which, granted, makes "y" designating the calendar year less than ideal as you'd want to pair Y with M, rather than y with M. Even more so as the 24-hour hour is H, so your standard ISO-8601 pattern is yyyy-MM-ddTHH:mm:ssZ which is... a bit of a mess casing-wise.
> Imagine an API which had methods 'getYear()' and 'GET_YEAR()' where the latter returns the payroll week year, while the former returns the, you know, actual year.
A big difference is the LDML pattern-space is rather more limited, and casing is definitely an important component of it.
> Which is why your op was saying this is like having an API where year() returns a surprising number thta isn't the year.
> You know, like JavaScript does.
If you want to rag on JS's datetime API, which really is Java's, first get in line, second getYear is hardly the worst offender (that belongs to the paired glue-eaters that are getDay and getDate).
But there's a little bit of a difference between mm/MM being minutes/months, where at least as someone thinking about datetime formatting you likely have a concept that the concepts of minutes and months are plausible things 'm' might stand for, vs. yyyy/YYYY where unless you've come across it before, the idea that there might be an inbuilt concept of a 'week year' is likely not going to be something that occurs to you.
I suppose the fact that both 'yyyy/mm/dd' and 'YYYY/MM/DD' produce values that are obviously wrong most of the time should be enough of an incentive for developers to go and look. up the codes. But that said, 'YYYY/MM/DD' produces what looks like the right answer for most of January...
Format strings are intended to be configurable (possibly per user-interface language) and not necessarily hardcoded. A strongly-typed builder API might be useful, but if you need both programmatic specification and external configuration, format strings can fulfill both purposes, whereas only having a builder API doesn’t.
More like `{numeric-year-no-leading-zeros}-{zero-padded-numeric-month}-{zero-padded-numeric-day-of-month}`. It’s virtually impossible to make it both succinct and unambiguous.
100% agreed with you. I do not understand why stringly typed date systems are still so prevalent.
When I write code that needs to format in different formats I always create well named, perhaps verbose but I don't care as it's now readable, functions such as this (ignore HN butchering the code):
/\*
\* Formats Date into "twelve hour time". For example, 3:23. This is "h:mm" format from date-fns.
\* @param { Date } date The date.
\* @returns { string } The formatted string.
\*/
export const formatAsTwelveHourTime = (date: Date) => format(date, 'h:mm');
Even as a proponent of strong typing, I fail to see how type system could have possibly helped in this case. Could you show some sort of example of how you envision types to be used here?
Verbose naming, yes, on the other hand would probably have made the situation clearer.
Have distinct types for each calendar (Gregorian, Julian, ISO week, Islamic, Jewish, Chinese, etc.) so it becomes obvious when you are mixing calendars.
IMHO we should be using explicit names in format strings such as `{year}`/`{week_year}` instead of remembering the differences between yyy, yyyy, YYYY and more.
"week year" was the cause of a big Twitter outage during the time I worked there. In the immediate aftermath there was a "see? this is why we need a monorepo" exhortation from leadership.
You configure that on the SimpleDateFormat’s Calendar (GregorianCalendar::setMinimalDaysInFirstWeek and setFirstDayOfWeek). The default is determined by the locale.
Another confusing thing is that in java's newer datetime api, java.time, yyyy is also incorrect as it means `year-of-era` while `u` means `year` so you would use `uuuu`
Since week year is so rarely used and since SimpleDateFormat() can't be changed perhaps the compiler could give a warning if it sees week year hardcoded in a call to SimpleDateFormat(). If its really wanted an annotation could turn off the warning.
This sort of thing gets developers thinking about how they could improve on the way dates and time are handled by various programming languages. Don't do it! I find it best to just change jobs whenever encountering any sort of task that requires dealing with dates, time, or worse - time zones.
At this point, there really should be some sort of compiler warning when using "YYYY" and "MM" in the same format string. GCC does vaguely similar things with printf-style formats.
> The reference time used in these layouts is the specific time stamp: 01/02 03:04:05PM '06 -0700 (January 2, 15:04:05, 2006, in time zone seven hours west of GMT). That value is recorded as the constant named Layout, listed below. As a Unix time, this is 1136239445.
Is it wrong to just use epoch seconds for practically everything, and view "The Date" as being a render/readonly thing? That should keep you safe, eliminates timezone madness, leap seconds, and all that craziness from time processing and quantification?
You know, assuming the earth's frame of reference....
93 comments
[ 3.3 ms ] story [ 180 ms ] thread- https://bugreport.java.com/bugreport/
edit: this wouldn't work, since W is reserved for weeks in month
If you use java, consider using google error-prone. It has a check for that (and many other things) https://errorprone.info/bugpattern/MisusedWeekYear
The last few days of 2021 logged as 2022 makes sense for payroll, but how the heck is Jan 1st 2022 put in the 2023 year?
This only happened shortly after midnight on January 1st. So I guess this is somehow related to different time zones/offsets.
2022-12-31 23:16:54.013
And then our UI converted this entry into one with 2023
(it correctly assumes that the string is in UTC and then converted it to local time +1h)
A linter should probably flag any format that mixes "week year" with month and days. It's really for use by itself or with payroll week number.
First off, I think you have a typo(?) with the "2023"? Because:
* https://www.epochconverter.com/weeks/2022
The reason(s) why the week system was designed:
> * All weeks have exactly 7 days, i.e. there are no fractional weeks.
> * Every week belongs to a single year, i.e. there are no ambiguous or double-assigned weeks.
> * The date directly tells the weekday.
> * All week-numbering years start with a Monday and end with a Sunday.
> * When used by itself without using the concept of month, all week-numbering years are the same except that some years have a week 53 at the end.
* https://en.wikipedia.org/wiki/ISO_week_date#Advantages
This makes perfect sense if your role is in bookkeeping / accounting / finance. Your life is based on pay periods and not on day-of-year/month. A particular pay day "belonging" to a particular year is a meaningless affectation: people need to get paid every two weeks come hell or high water, and anything that can complicate the smooth running of that process is discarded.
As for the algorithm:
> The ISO 8601 definition for week 01 is the week with the first Thursday of the Gregorian year (i.e. of January) in it. The following definitions based on properties of this week are mutually equivalent, since the ISO week starts with Monday: […]
* https://en.wikipedia.org/wiki/ISO_week_date#First_week
It's not just payrolls, it's also things like deliveries or synchronisation. And saying "monday week 15" (or writing "W15 mon") aside from being short and precise can give a better ground for synchronisation or collision than "April 11th" e.g. the person you're talking to might know that every monday would be an issue and be able to say that that's not an option before even looking it up.
It's not, it's a fiscal, or more generally business creation, allowing businesses to express everything in weekly basis, with no variance (all weeks are 7 days mon-sun with no fractionals, and every week belongs to a single year with no ambiguity).
This is very, very common usage in european businesses, and even casual settings, to the extent that you'll have a hard time finding a physical calendar without weeks printed on it.
Format specifiers shouldn’t be case sensitive. Better mnemonics would be even better.
I would take `format(year(), '/', month(), '/', day())` over ad-hoc format strings by various APIs.
Reading the docs further this also stands out:
> For parsing with the abbreviated year pattern ("y" or "yy"), SimpleDateFormat must interpret the abbreviated year relative to some century. It does this by adjusting dates to be within 80 years before and 20 years after the time the SimpleDateFormat instance is created. For example, using a pattern of "MM/dd/yy" and a SimpleDateFormat instance created on Jan 1, 1997, the string "01/11/12" would be interpreted as Jan 11, 2012 while the string "05/04/64" would be interpreted as May 4, 1964. During parsing, only strings consisting of exactly two digits, as defined by Character.isDigit(char), will be parsed into the default century. Any other numeric string, such as a one digit string, a three or more digit string, or a two digit string that isn't all digits (for example, "-1"), is interpreted literally. So "01/02/3" or "01/02/003" are parsed, using the same pattern, as Jan 2, 3 AD. Likewise, "01/02/-3" is parsed as Jan 2, 4 BC.
I hope nobody uses this to parse historical data that happens to become older than 80 years recently.
with your example:
> format_description!("[year]/[month]/[day]")
0: https://crates.io/crates/time
1: https://time-rs.github.io/book/api/format-description.html
I don’t think the language has anything suitable for these purposes. What did you have in mind?
That components have parameters makes the non-literal approach even less compelling: take this which can produce the likes of “2:34:56pm”:
For reference, that is equivalent to this: You could easily provide a prettier DSL so that you could write something like this: This wouldn’t be awful in the absence of the parse method, but really, once you have that, the format_description macro is just what you want: compact, checked at compile time, and matching a runtime equivalent which can take user-provided format strings.(Now there are two or three changes I’d prefer to make to format_description’s syntax: I’d use = instead of :, the two being generally very similar but : far more regularly occurring in literal parts, so that the different = would make it scan better; and I think that escaping opening square brackets by doubling them but not requiring doubling for closing square brackets was a particularly bad idea; and I’m mildly inclined to prefer {} to []. So I might end up with "{hour padding=none repr=12}:{minute}:{second}{period case=lower}".)
Yes, but it would not have helped with the bug in question. The parallel bug for a strongly typed interface would be "year()" returning this monstrosity, while "iso_year()" or some other poorly named variant returning the expected year.
No API is immune to footguns and bad design decisions.
On the other hand the much less often used "year of week" would be named "year_of_week()" and hence it is clear to everyone that's not likely what you want.
That would be the sane thing to do. But the same applies to `YYYY`: it should be used for "year of era". But it hasn't been and that's the problem here. For contrast in moment.js, YYYY and yyyy do what you expect and "week year" is GGGG or gggg.
Why? `yyyy` is simpler to type, so makes a lot more sense for "year of era".
> For contrast in moment.js, YYYY and yyyy do what you expect and "week year" is GGGG or gggg.
In LDML (which I assume is what SimpleDateFormat uses), the G field is already spoken for the era name (BC/BCE and AD/CE).
Turns out I had to use getFullYear, which is of course perfectly intuitive.
Even when javascript was written (and likewise with things like perl and php - and of course the underlying function dated back to the 70s ), it was still quite common for people to write dates by hand as 6/6/96 for June 6th 1996, hence people would often write something like
print day() + "/" + mon() + "/" + year()
If you wanted to put a 4 digit year many people
print day() + "/" + mon() + "/19" + year()
After 2000 came round, many sites would claim the year was 19100, I still saw these sites until well into the late 00s
However some people wrote print day() + "/" + mon() + "/" + (1900 + year())
Which would give the correct display.
Changing year() from returning a 2 digit (which in the 80s and even in the 90s was often preferable) to 4 digits would be a breaking change. Changing it to being "the last two digits" rather than "number of years since 1900" would be a breaking change. That's not something that people like to do, hence things like "getFullYear" to return a 4 digit year (or 3 digit for <1000 etc)
This kind of feature[1] lead to many potential Y2K bugs.
[1] Which it shares with Perl and probably many other languages.
The actual result for that date is "122-0-2" - not a single component of the date was what I expected.
I'm sure you've read the docs since then but just for the viewers at home:
s/getYear/getFullYear
s/getDay/getDate
getYear is oldfashioned, years since 1900. getDay is day-of-the-week, 0 for Sunday, 1 for Monday, etc
Why, if everything else in JS is 0 indexed? Isn't consistency a good thing in a language? If they'd gone the other way, wouldn't there be people here complaining that 1 indexing is whack?
lol
No, the parallel for this would be called `week_year()`, or `iso_week_year()`, to be paired with an `iso_week()` (or `year_week()` since there really is no other formal week-year).
Imagine an API which had methods 'getYear()' and 'GET_YEAR()' where the latter returns the payroll week year, while the former returns the, you know, actual year.
That's what having YYYY produce the week year feels like - a blatant violation of the principle of least surprise.
Which is why your op was saying this is like having an API where year() returns a surprising number thta isn't the year.
You know, like JavaScript does.
There's the part that the entire LDML datetime grammar works like that.
Literally the next letter in your pattern will make it clear: M is the month, and m is the minute.
Which, granted, makes "y" designating the calendar year less than ideal as you'd want to pair Y with M, rather than y with M. Even more so as the 24-hour hour is H, so your standard ISO-8601 pattern is yyyy-MM-ddTHH:mm:ssZ which is... a bit of a mess casing-wise.
> Imagine an API which had methods 'getYear()' and 'GET_YEAR()' where the latter returns the payroll week year, while the former returns the, you know, actual year.
A big difference is the LDML pattern-space is rather more limited, and casing is definitely an important component of it.
> Which is why your op was saying this is like having an API where year() returns a surprising number thta isn't the year.
> You know, like JavaScript does.
If you want to rag on JS's datetime API, which really is Java's, first get in line, second getYear is hardly the worst offender (that belongs to the paired glue-eaters that are getDay and getDate).
But there's a little bit of a difference between mm/MM being minutes/months, where at least as someone thinking about datetime formatting you likely have a concept that the concepts of minutes and months are plausible things 'm' might stand for, vs. yyyy/YYYY where unless you've come across it before, the idea that there might be an inbuilt concept of a 'week year' is likely not going to be something that occurs to you.
I suppose the fact that both 'yyyy/mm/dd' and 'YYYY/MM/DD' produce values that are obviously wrong most of the time should be enough of an incentive for developers to go and look. up the codes. But that said, 'YYYY/MM/DD' produces what looks like the right answer for most of January...
When I write code that needs to format in different formats I always create well named, perhaps verbose but I don't care as it's now readable, functions such as this (ignore HN butchering the code):
Verbose naming, yes, on the other hand would probably have made the situation clearer.
> The reference time used in these layouts is the specific time stamp: 01/02 03:04:05PM '06 -0700 (January 2, 15:04:05, 2006, in time zone seven hours west of GMT). That value is recorded as the constant named Layout, listed below. As a Unix time, this is 1136239445.
> RFC3339 = "2006-01-02T15:04:05Z07:00"
https://golang.org/pkg/time/
You know, assuming the earth's frame of reference....
Display timestamps however the user wants.
Do not roll your own timestamp code. See "Falsehoods programmers believe about time".