-
Notifications
You must be signed in to change notification settings - Fork 37
Multi calendar system
The JSR-310 API is built on the principle that the primary calendar system in the world is the ISO-8601 system. All key APIs, such a LocalDate and OffsetDateTime are built around this.
The rationale is that most developers use the de-facto civil calendar (ISO-8601) in the daily lives and in their coding lives. Almost all global business, network traffic and storage (databases) uses either ISO-8601 or a simple count from a fixed epoch (the Instant class in JSR-310). Alternate calendar systems are primarily a localization issue, focused on user input/output. Only occasionally do calculations need to be performed in other calendar systems.
The .NET framework manages dates and times in a similar manner. The .NET DateTime
class represents an ISO-8601 date/time, while a separate Calendar
helper class is used to access other calendar systems.
The key criteria any proposal will be judged on are as follows:
A) Overall API clarity for the common user: Most users want ISO-8601, other calendars are a special use case, and those that want them already know more about the date/time problem domain. Anything that causes the common user to stop and have doubts is a negative here. A specialist API for other calendar systems is perfectly acceptable.
B) Methods must be precisely defined for the common user: No method that normally returns a month from 1 to 12 may return 1 to 13 simply because some other setting has changed the calendar system. In other words methods like getMonth() or getYear() must standalone with a totally clear and unambiguous meaning.
C) The state of value object must be clean and pure. A value object is defined by its state more than anything else. The state of a date is clear, what changes is the representation of the date. A date object, such as LocalDate
, must only store the minimal state corresponding to the date. In essence this is the number of days from a fixed epoch, however it may be more effective to store year/month/day.
D) The state is expressed in the toString
, equals
and compareTo
. A value type will have each of these methods fully and simply defined to express and use all of the state of the object.
E) There should not be an excess of classes, nor should classes for the other calendar system be confused for the main ISO-8601 classes. This is important so that all common users pick up and use the correct API and do not concern themselves with other calendar systems. It just works.
F) The other calendar system API should still retain an element of good design even though it is distinct. Real users will be using it, and even if they have to work a little harder, they shouldn't have to suffer. ie. its OK to have to learn the other calendar system API, whereas the ISO API must be instantly usable.
A requirement has been expressed to have support for offset and zoned forms of date-times in other calendar systems. Part of the driver for this is matching the API in the JDK today. Further comments on these use cases can be found in the comments of https://github.com/ThreeTen/threeten/issues/48.
It is to be noted that many other calendar systems start their days at sunset or sunrise, thus the same time-of-day occurs twice or not-at-all on any given day. It is agreed that this subtlety can be ignored and other calendar systems can be treated as starting their day at midnight.
Joda-Time fails these criteria. The getYear()
, getMonthOfYear()
methods and so on do not necessarily return the expected values and they are calculated wrt the chronology, a separate field stored in the object.
The Joda-Time class does not display the chronology in the toString()
method, but does use it in the equals()
method, a source of confusion when tests fail but the toString()
is the same.
1) ChronoDate
Continue with the current approach, where a separate value object encapsulates the combination of a date and a calendar system. The problem here is that there is a push to add more and more methods to ChronoDate
. There is also a desire to see support for logical representations of months and eras using real types.
This approach can be extended to have a single ChronoZonedDateTime class to handle the additional requirement. The offset cases would be handled using the fixed offset zone ids.
This approach meets criteria A, B, C, D and F, but is marginal in E.
2) Hierarchy
Proposal to make LocalDate
extend ChronoDate
. This would have an immediate impact of adding the state of the calendar system to LocalDate
, which changes the essence of the state of the class. Logically, it should include the calendar system name in the toString
which is undesirable. While this can be avoided it is a sympton of deeper issues.
LocalDate
is stored within LocalDateTime
. The proposal suggests that LocalDateTime
becomes ChronoDateTime
, now combining ChronoDate
and LocalTime
. Again, this changes the state. More specifically it also changes the meaning of the API methods. The only way to preserve the menaing is to have a subclass LocalDateTime
.
The same logic applies to OffsetDate
, OffsetDateTime
and ZonedDateTime
. Thus, making LocalDate
extend ChronoDate
requires an additional 4 classes to ensure that the JSR-310 criteria on method name meanings are preserved.
More of a concern is that users would tend to use the Chrono* classes when they should use the ISO classes. This expectation is based on the developer "trying to do the right thing". Developers today might start by using GregorianCalendar
. They then get told or read that they should declare their variables/parameters as Calendar
because it makes their application better (more capable of I18N). The developer now thinks that they have improved their application and goes home happy. However, they've actually made things worse. If, at some point in the future, someone passes in an object of a type other than GregorianCalendar
, in many/most cases the code will now break. Thats because the developer will actually have made some subtle assumptions around their usage of the Calendar
that are actually ISO specific. This is why any superclass is a problem - because developers will use it, and they really shouldn't usless they are I18N trained.
This approach fails all criteria except F, but could just about meet A and B if the user ignored the supertypes (aomething I don't believe to be possible given how j.u.Calendar is used today).
3) Functionality only on Chronology Based on .NET, this proposal would remove ChronoDate and only have methods on Chronology.
LocalDate dt = ...
year = chronology.get(dt, YEAR);
dt = chronology.add(dt, 2, MONTHS);
dt = chronology.set(dt, DAY_OF_MONTH, 1);
This approach is very simple to implement and separates the multi-calendar system API from the main API. However it may be hard to integrate into the formatter code.
This approach meets all criteria, except F.
4) No classes - Parameterized functions
Proposal to alter the API of all DateTime
implementations to add new methods parameterized by the calendar system.
interface DateTime {
long get(DateTimeField)
long get(DateTimeField, Chronology)
DateTime with(DateTimeField, long)
DateTime with(DateTimeField, long, Chronology)
// similar for plus/minus
}
LocalDate dt = ...
year = dt.get(YEAR, chrono);
dt = dt.plus(2, MONTHS, chrono).with(DAY_OF_MONTH, 1, chrono)
The application would be responsible for passing in the chronology when required. This fits well with the notion of calendar systems being primarily a UI-level feature.
This approach is very simple and immediately understandable. It involves minimal extra code at the API layer. The implementation of non-ISO get/with would be delegated to the chronology.
The downside is that the application is responsible for managing the chronology if it needs to. Normally this won't be a problem, however it would have to pass it in to each date-time manipulation method.
This approach meets all criteria, except F.
5) Chrono wrapper
This proposal builds on option 3 to add a wrapper class Chrono
. This would be created around an underlying ISO date-time object and alter the date methods.
class Chrono<T extends AdjustableDateTime> {
private T underlying;
private Chronology chrono;
@Override
public long get(DateTimeField field) {
return chrono.get(underlying, field);
}
}
Chrono<LocalDate> dt = Chrono.of(localDate);
int year = dt.get(YEAR);
dt = date.plus(2, MONTHS).with(DAY_OF_MONTH, 1);
This approach provides a single class that can wrap any other date-time class and add knowledge of the other calendar system. It would be advisable to keep this class simple and not try to make it a full equivalent of LocalDate
et al.
This meets the additional requirement of Chrono<ZonedDateTime>
.
It would be possible to create an instance of Chrono<LocalTime>
, which is rather non-sensical. This could be restricted if necessary, however it probably doesn't need to be restricted.
This meets all the criteria.
The ISO-8601 classes benefit from access to the Month
class. Other calendars systems have three options here:
- only use an
int
value - create a single "month description" class
- create a month enum or similar class for each calendar system
The first of the three options is by far the simplest to manage and handles the text input/output use cases in combination with a formatter.
A similar pattern applies to the era field. Again an int
value is the simplest to implement and covers the use cases.