Appointment class
I'm trying to create a method that would add an appointment to an arraylist appointmentCalndar. This method would validate the date to see if the user input is equal to the SimpleDateFormat in my code, the startTime and the endTime of the appointment and to see if its in the future or not.
I have tried using the java Date api to check this but when I try to extend the class to get access to the attributes it always causes an error on compile time. So overall my question is what would the best way to compare an object of appointment type to an object of type date? I try using accesors to getDate() and startTime and endTime but it won't allow me to get them also.
public AppointmentDate(String appString)
{
// 1) split ithe string into Date/from/to
// 2) consturct the Date object for the appDate
// 3) consturct the Date object for the startTime
// 4) consturct the Date object for the endTime
String appDetails = appString.split(",");
if(appDetails.length == 2)
{
try {
SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
this.appDate = df.parse(appDetails[0]);
DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy,mm:HH");
String dFormat = appDetails[0] + "," + appDetails[1];
this.startTime = formatter.parse(dFormat);
dFormat = appDetails[0] + "," + appDetails[2];
this.endTime = formatter.parse(dFormat);
}
catch (Exception ex)
{
}
}
else
{
System.out.print("User Date is Invalid");
}
}
public void setStartTime(Date startTime)
{
this.startTime = startTime;
}
public Date getStartTime()
{
return startTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
public void setAppdate(Date appDate)
{
this.appDate = appDate;
}
public Date getAppDate()
{
return appDate;
}
public void add(Appointment a)
{
if (a.equals(a.getDate()))
{
if(a.getStartTime() < a.getEndTime())
{
}
}
else
{
System.out.print("");
}
}
java date object
add a comment |
I'm trying to create a method that would add an appointment to an arraylist appointmentCalndar. This method would validate the date to see if the user input is equal to the SimpleDateFormat in my code, the startTime and the endTime of the appointment and to see if its in the future or not.
I have tried using the java Date api to check this but when I try to extend the class to get access to the attributes it always causes an error on compile time. So overall my question is what would the best way to compare an object of appointment type to an object of type date? I try using accesors to getDate() and startTime and endTime but it won't allow me to get them also.
public AppointmentDate(String appString)
{
// 1) split ithe string into Date/from/to
// 2) consturct the Date object for the appDate
// 3) consturct the Date object for the startTime
// 4) consturct the Date object for the endTime
String appDetails = appString.split(",");
if(appDetails.length == 2)
{
try {
SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
this.appDate = df.parse(appDetails[0]);
DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy,mm:HH");
String dFormat = appDetails[0] + "," + appDetails[1];
this.startTime = formatter.parse(dFormat);
dFormat = appDetails[0] + "," + appDetails[2];
this.endTime = formatter.parse(dFormat);
}
catch (Exception ex)
{
}
}
else
{
System.out.print("User Date is Invalid");
}
}
public void setStartTime(Date startTime)
{
this.startTime = startTime;
}
public Date getStartTime()
{
return startTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
public void setAppdate(Date appDate)
{
this.appDate = appDate;
}
public Date getAppDate()
{
return appDate;
}
public void add(Appointment a)
{
if (a.equals(a.getDate()))
{
if(a.getStartTime() < a.getEndTime())
{
}
}
else
{
System.out.print("");
}
}
java date object
Why do you not just useDate
as it is? Why want you to compare anAppointment
(containing start and end date) with a singleDate
object?
– Michael Butscher
Nov 20 '18 at 23:23
1
Use LocalDate (or LocalDateTIme) it's easier ;)=
– azro
Nov 20 '18 at 23:26
add a comment |
I'm trying to create a method that would add an appointment to an arraylist appointmentCalndar. This method would validate the date to see if the user input is equal to the SimpleDateFormat in my code, the startTime and the endTime of the appointment and to see if its in the future or not.
I have tried using the java Date api to check this but when I try to extend the class to get access to the attributes it always causes an error on compile time. So overall my question is what would the best way to compare an object of appointment type to an object of type date? I try using accesors to getDate() and startTime and endTime but it won't allow me to get them also.
public AppointmentDate(String appString)
{
// 1) split ithe string into Date/from/to
// 2) consturct the Date object for the appDate
// 3) consturct the Date object for the startTime
// 4) consturct the Date object for the endTime
String appDetails = appString.split(",");
if(appDetails.length == 2)
{
try {
SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
this.appDate = df.parse(appDetails[0]);
DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy,mm:HH");
String dFormat = appDetails[0] + "," + appDetails[1];
this.startTime = formatter.parse(dFormat);
dFormat = appDetails[0] + "," + appDetails[2];
this.endTime = formatter.parse(dFormat);
}
catch (Exception ex)
{
}
}
else
{
System.out.print("User Date is Invalid");
}
}
public void setStartTime(Date startTime)
{
this.startTime = startTime;
}
public Date getStartTime()
{
return startTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
public void setAppdate(Date appDate)
{
this.appDate = appDate;
}
public Date getAppDate()
{
return appDate;
}
public void add(Appointment a)
{
if (a.equals(a.getDate()))
{
if(a.getStartTime() < a.getEndTime())
{
}
}
else
{
System.out.print("");
}
}
java date object
I'm trying to create a method that would add an appointment to an arraylist appointmentCalndar. This method would validate the date to see if the user input is equal to the SimpleDateFormat in my code, the startTime and the endTime of the appointment and to see if its in the future or not.
I have tried using the java Date api to check this but when I try to extend the class to get access to the attributes it always causes an error on compile time. So overall my question is what would the best way to compare an object of appointment type to an object of type date? I try using accesors to getDate() and startTime and endTime but it won't allow me to get them also.
public AppointmentDate(String appString)
{
// 1) split ithe string into Date/from/to
// 2) consturct the Date object for the appDate
// 3) consturct the Date object for the startTime
// 4) consturct the Date object for the endTime
String appDetails = appString.split(",");
if(appDetails.length == 2)
{
try {
SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
this.appDate = df.parse(appDetails[0]);
DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy,mm:HH");
String dFormat = appDetails[0] + "," + appDetails[1];
this.startTime = formatter.parse(dFormat);
dFormat = appDetails[0] + "," + appDetails[2];
this.endTime = formatter.parse(dFormat);
}
catch (Exception ex)
{
}
}
else
{
System.out.print("User Date is Invalid");
}
}
public void setStartTime(Date startTime)
{
this.startTime = startTime;
}
public Date getStartTime()
{
return startTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
public void setAppdate(Date appDate)
{
this.appDate = appDate;
}
public Date getAppDate()
{
return appDate;
}
public void add(Appointment a)
{
if (a.equals(a.getDate()))
{
if(a.getStartTime() < a.getEndTime())
{
}
}
else
{
System.out.print("");
}
}
java date object
java date object
asked Nov 20 '18 at 23:15
Aaron LongAaron Long
153
153
Why do you not just useDate
as it is? Why want you to compare anAppointment
(containing start and end date) with a singleDate
object?
– Michael Butscher
Nov 20 '18 at 23:23
1
Use LocalDate (or LocalDateTIme) it's easier ;)=
– azro
Nov 20 '18 at 23:26
add a comment |
Why do you not just useDate
as it is? Why want you to compare anAppointment
(containing start and end date) with a singleDate
object?
– Michael Butscher
Nov 20 '18 at 23:23
1
Use LocalDate (or LocalDateTIme) it's easier ;)=
– azro
Nov 20 '18 at 23:26
Why do you not just use
Date
as it is? Why want you to compare an Appointment
(containing start and end date) with a single Date
object?– Michael Butscher
Nov 20 '18 at 23:23
Why do you not just use
Date
as it is? Why want you to compare an Appointment
(containing start and end date) with a single Date
object?– Michael Butscher
Nov 20 '18 at 23:23
1
1
Use LocalDate (or LocalDateTIme) it's easier ;)=
– azro
Nov 20 '18 at 23:26
Use LocalDate (or LocalDateTIme) it's easier ;)=
– azro
Nov 20 '18 at 23:26
add a comment |
1 Answer
1
active
oldest
votes
Static block (almost)
Your code to exercise the class is in the wrong place. You have it stuck at the top of the class which is not syntactically correct. We can run code at the top, as a static block, but it needs to be labeled static { … }
. A static block is not commonly used in my experience. And certainly is not the right place for what you are doing there.
main
method
Instead you should be using a main
method. This non-OOP little thingie is a trick, a hack, to solve the chicken-or-the-egg conundrum, to get us from no-app-running to our OOP idea of heaven with a bunch of objects floating around and passing messages to one another.
When first learning Java, do not try to understand all the syntax and purpose of the main
method. Just accept it as a necessary evil to get the app running, it is merely the entrance point to execute the app. Focus on learning the OOP concepts and practices. Later, the main
method and syntax will make more sense.
Accessors
Here is a simplified re-write of your example code. We are just using a LocalDate
for simplicity, just enough to show (a) a main
method, and (b) getter/setter accessor methods.
package com.basilbourque.example;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AppointmentDate {
private LocalDate localDate;
// Constructor
public AppointmentDate ( LocalDate localDate ) {
this.localDate = localDate;
}
public LocalDate getLocalDate ( ) {
return localDate;
}
public void setLocalDate ( LocalDate localDate ) {
this.localDate = localDate;
}
@Override
public String toString ( ) {
return "AppointmentDate{ " +
"localDate=" + localDate +
" }";
}
// Not really a part of this class. A `main` method is just a hack to get our app launched.
public static void main ( String args ) {
String input = "23/01/2018";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" );
LocalDate ld = LocalDate.parse( input , f );
AppointmentDate ad = new AppointmentDate( ld );
ad.setLocalDate( ld.plusWeeks( 1 ) );
LocalDate newValue = ad.getLocalDate();
System.out.println( newValue.toString() ); // Generate text representing the value of this `LocalDate` object in standard ISO 8601 format.
List < AppointmentDate > list = new ArrayList <>( 3 );
list.add( ad );
list.add( new AppointmentDate( LocalDate.parse( "2018-02-13" ) ) );
list.add( new AppointmentDate( LocalDate.parse( "2018-12-21" ) ) );
System.out.println( list );
}
}
2018-01-30
[AppointmentDate{ localDate=2018-01-30 }, AppointmentDate{ localDate=2018-02-13 }, AppointmentDate{ localDate=2018-12-21 }]
java.time
You are using terrible old date-time classes that were supplanted years ago by the java.time classes. Never use Date
, Calendar
, SimpleDateFormat
, etc.
Appointments are tricky
While appointment tracking may seem intuitively simple, you are actually working on a very tricky subject.
The core problem is that politicians around the world are fond of redefining the time zone(s) under their jurisdiction. They do so quite frequently. They do so in both times of relative quiet and in times of turmoil.
The US and Canada have changed their offsets multiple times in recent decades. Turkey and Russia have changed their minds about going on or off DST multiple times in the last several years.
And politicians change their time zones with very little advance notice. And the notice seems to be growing shorter, despite the increased disturbance this causes in ever-more-computerized societies. Just last month, Morocco announced their country would stay on Daylight Saving Time (DST) permanent, cancelling on a Friday the DST cutover scheduled for that Sunday, leaving 0 business days warning — what a mess for IT staff. In May this year, North Korea slipped their clock a half-hour to sync with South Korea, with apparently immediate effect (no advance notice at all).
These frequent and unpredictable changes mean we cannot responsibly track future appointments as moments, as specific points on the timeline. When we say something like “3 PM on January 23rd” we usually mean 3 PM after politicians may have made their changes to the clock.
So we must store future appointments as a date and a time-of-day without a time zone or offset-from-UTC. Then, when calculating a calendar we must dynamically apply the rules for the intended time zone as currently defined on that day. If we perform this dynamic-determination of a moment today, and again in three days, if the politicians have announced a change in the time zone definition, and if we have been able to update our tzdata
data files in our operating systems, database engines, Java Virtual Machines, and various libraries, then we will arrive at a different moment in time.
LocalDateTime
The Local…
types in Java purposely lack any concept of time zone or offset-from-UTC. So they cannot represent a moment. So we never use these to pinpoint actual events that happened in the past. But these types are what we need for future appointments.
The LocalDateTime
class represents a date with a time-of-day without any zone/offset.
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
LocalTime lt = LocalTime.of( 15 , 0 ) ; // 3 PM in 24-hour time.
LocalDateTime ldt= LocalDateTime.of( ld , lt ) ;
ZonedDateTime
When calculating a calendar, when we need a specific moment, we apply a time zone (ZoneId
) to get a ZonedDateTime
object.
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ; // Determine a moment, a specific point on the timeline.
Instant
We can view that same moment in UTC by extracting a Instant
.
Instant instant = zdt.toInstant() ; // Adjust to UTC.
Duration
Appointments are generally best stored as a starting point plus a duration. No need to store the stopping point as that can be calculated.
Duration d = Duration.ofHours( 1 ) ; // A one-hour appointment.
While we often want to adjust into a time zone for presentation to a user, generally behind-the-scenes it is best practice to track moments in UTC. So the starting and stopping points of an appointment calculated as moments should be done as a pair of Instant
objects.
Instant start = ldt.atZone( z ).toInstant() ;
Instant stop = start.plus( d ) ;
Interval
We can leverage a class to represent this pair of Instant
objects, Interval
.
This class is found in the ThreeTen-Extra library, a project led by the same man than led the Joda-Time, JSR 310, and java.time projects, Stephen Colebourne.
This class has very handy methods for comparison such as abuts
, overlaps
, contains
, and so on. You will likely want to use these methods in a scheduling app.
Appointment.java
Put this all together and we get a class like this:
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Might add some getter/setter methods in here.
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
}
When you generate a Interval
by calling your toInterval
method, you may want the individual start and stop moments.
Instant start = interval.getStart() ;
Instant stop = interval.getEnd() ;
Those two Instant
objects are in UTC by definition. If you want to see them through the lens of the wall-clock time used by the people of a particular region, apply a ZoneId
to get a ZonedDateTime
object.
ZoneId zAuckland = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdtStart = start.atZone( z ) ; // Adjust from UTC to some time zone. Same moment, same point on the timeline, different wall-clock time.
ZonedDateTime zdtStop = stop.atZone( z ) ;
Future
You asked about checking to see if this appointment is in the future. Again, we need a time zone to properly answer than. The time zones around the world currently cover a range of about 26 to 27 hours. So within in that many hours of the current moment we cannot tell if a LocalDateTime
is in the future or past without considering a time zone.
So let's add a method testing for the future that requires passing a time zone.
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5.");
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture ;
}
Start/Stop moments
Continuing the same theme on dynamically determining moments, let's add some methods to return the starting moment (inclusive) and stopping moment (exclusive). As discussed above, this requires passing a time zone.
The calling programmer could do this work herself. But I suspect this might be frequently needed enough to warrant adding these methods as a convenience.
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
Notice that I did not name these methods with the get…
. Accessor methods, getters & setters, by convention imply accessing a simple property stored within the object. But here we are not storing the ZonedDateTime
objects. These are dynamically-determined, so using a get…
method could be misleading. Instead, I am trying to follow the naming conventions laid down in the java.time project.
Immutable objects
Another lesson to learn from the java.time project is the immutable objects pattern.
Certain kinds of classes lend themselves to being read-only, created but not modified. The java.time classes certainly qualify. Whereas an invoice, for example, is expected to “mutate” (change), intuitively as a programmer I do not expect the date on the invoice to change unless I explicitly replace the date with a new object. So I want invoice to be a mutable object, but I want the LocalDate
object stored on that invoice to be immutable.
I suspect our Appointment
class might also be best designed an an immutable. So we have no setter
methods involved. To effectively alter an appointment in your scheduling app, create a new Appointment
object based on some of the values of the existing Appointment
object. Notice in the java.time classes how this is done with various with
methods, where the methods return a new object based on the original’s values but with some alteration.
Appointment.java
version 2
Let's put all that together into one example class.
And let's add a main
method to exercise this class. First we create one appointment, and look at its dynamically-determined moments in UTC. Second, we collect some Appointment
objects in a collection.
We have added a toString
method override to report on the object’s state.
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// An example class to show date-time handling for future appointments.
// Not necessarily ready for production use. Use at your own risk.
// Methods named according to the java.time naming conventions:
// https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to get the start/stop interval of an appointment. Message # bbf021e6-baa7-468d-83ad-cf73acb6702e." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture;
}
// -----------| Object overrides |---------------------------
@Override
public String toString ( ) {
return "Appointment{ " +
"start=" + start +
" | duration=" + duration +
" }";
}
// -----------| main |-------------
public static void main ( String args ) {
// See if a new appointment is in the future.
Appointment a = new Appointment( LocalDateTime.of( 2018 , 12 , 25 , 0 , 0 , 0 , 0 ) , Duration.ofHours( 2 ) );
ZoneId z = ZoneId.of( "America/Montreal" );
System.out.println( "For time zone: " + z + ", appointment interval is: " + a.toInterval( z ) );
System.out.println( "Start: " + a.toStartMoment( z ) );
System.out.println( "Stop: " + a.toStopMoment( z ) );
Boolean isFuture = a.isFuture( z );
System.out.println( a.toString() + " is future t/f: " + isFuture );
// Collect some appointments.
List < Appointment > list = new ArrayList <>( 3 );
list.add( a );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 13 , 15 , 0 , 0 , 0 ) , Duration.ofMinutes( 90 ) ) );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 30 , 16 , 0 , 0 , 0 ) , Duration.ofHours( 1 ) ) );
System.out.println( list );
}
}
When run.
For time zone: America/Montreal, appointment interval is: 2018-12-25T05:00:00Z/2018-12-25T07:00:00Z
Start: 2018-12-25T00:00-05:00[America/Montreal]
Stop: 2018-12-25T02:00-05:00[America/Montreal]
Appointment{ start=2018-12-25T00:00 | duration=PT2H } is future t/f: true
[Appointment{ start=2018-12-25T00:00 | duration=PT2H }, Appointment{ start=2018-12-13T15:00 | duration=PT1H30M }, Appointment{ start=2018-12-30T16:00 | duration=PT1H }]
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
- Later versions of Android bundle implementations of the java.time classes.
- For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.
1
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53403042%2fappointment-class%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Static block (almost)
Your code to exercise the class is in the wrong place. You have it stuck at the top of the class which is not syntactically correct. We can run code at the top, as a static block, but it needs to be labeled static { … }
. A static block is not commonly used in my experience. And certainly is not the right place for what you are doing there.
main
method
Instead you should be using a main
method. This non-OOP little thingie is a trick, a hack, to solve the chicken-or-the-egg conundrum, to get us from no-app-running to our OOP idea of heaven with a bunch of objects floating around and passing messages to one another.
When first learning Java, do not try to understand all the syntax and purpose of the main
method. Just accept it as a necessary evil to get the app running, it is merely the entrance point to execute the app. Focus on learning the OOP concepts and practices. Later, the main
method and syntax will make more sense.
Accessors
Here is a simplified re-write of your example code. We are just using a LocalDate
for simplicity, just enough to show (a) a main
method, and (b) getter/setter accessor methods.
package com.basilbourque.example;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AppointmentDate {
private LocalDate localDate;
// Constructor
public AppointmentDate ( LocalDate localDate ) {
this.localDate = localDate;
}
public LocalDate getLocalDate ( ) {
return localDate;
}
public void setLocalDate ( LocalDate localDate ) {
this.localDate = localDate;
}
@Override
public String toString ( ) {
return "AppointmentDate{ " +
"localDate=" + localDate +
" }";
}
// Not really a part of this class. A `main` method is just a hack to get our app launched.
public static void main ( String args ) {
String input = "23/01/2018";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" );
LocalDate ld = LocalDate.parse( input , f );
AppointmentDate ad = new AppointmentDate( ld );
ad.setLocalDate( ld.plusWeeks( 1 ) );
LocalDate newValue = ad.getLocalDate();
System.out.println( newValue.toString() ); // Generate text representing the value of this `LocalDate` object in standard ISO 8601 format.
List < AppointmentDate > list = new ArrayList <>( 3 );
list.add( ad );
list.add( new AppointmentDate( LocalDate.parse( "2018-02-13" ) ) );
list.add( new AppointmentDate( LocalDate.parse( "2018-12-21" ) ) );
System.out.println( list );
}
}
2018-01-30
[AppointmentDate{ localDate=2018-01-30 }, AppointmentDate{ localDate=2018-02-13 }, AppointmentDate{ localDate=2018-12-21 }]
java.time
You are using terrible old date-time classes that were supplanted years ago by the java.time classes. Never use Date
, Calendar
, SimpleDateFormat
, etc.
Appointments are tricky
While appointment tracking may seem intuitively simple, you are actually working on a very tricky subject.
The core problem is that politicians around the world are fond of redefining the time zone(s) under their jurisdiction. They do so quite frequently. They do so in both times of relative quiet and in times of turmoil.
The US and Canada have changed their offsets multiple times in recent decades. Turkey and Russia have changed their minds about going on or off DST multiple times in the last several years.
And politicians change their time zones with very little advance notice. And the notice seems to be growing shorter, despite the increased disturbance this causes in ever-more-computerized societies. Just last month, Morocco announced their country would stay on Daylight Saving Time (DST) permanent, cancelling on a Friday the DST cutover scheduled for that Sunday, leaving 0 business days warning — what a mess for IT staff. In May this year, North Korea slipped their clock a half-hour to sync with South Korea, with apparently immediate effect (no advance notice at all).
These frequent and unpredictable changes mean we cannot responsibly track future appointments as moments, as specific points on the timeline. When we say something like “3 PM on January 23rd” we usually mean 3 PM after politicians may have made their changes to the clock.
So we must store future appointments as a date and a time-of-day without a time zone or offset-from-UTC. Then, when calculating a calendar we must dynamically apply the rules for the intended time zone as currently defined on that day. If we perform this dynamic-determination of a moment today, and again in three days, if the politicians have announced a change in the time zone definition, and if we have been able to update our tzdata
data files in our operating systems, database engines, Java Virtual Machines, and various libraries, then we will arrive at a different moment in time.
LocalDateTime
The Local…
types in Java purposely lack any concept of time zone or offset-from-UTC. So they cannot represent a moment. So we never use these to pinpoint actual events that happened in the past. But these types are what we need for future appointments.
The LocalDateTime
class represents a date with a time-of-day without any zone/offset.
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
LocalTime lt = LocalTime.of( 15 , 0 ) ; // 3 PM in 24-hour time.
LocalDateTime ldt= LocalDateTime.of( ld , lt ) ;
ZonedDateTime
When calculating a calendar, when we need a specific moment, we apply a time zone (ZoneId
) to get a ZonedDateTime
object.
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ; // Determine a moment, a specific point on the timeline.
Instant
We can view that same moment in UTC by extracting a Instant
.
Instant instant = zdt.toInstant() ; // Adjust to UTC.
Duration
Appointments are generally best stored as a starting point plus a duration. No need to store the stopping point as that can be calculated.
Duration d = Duration.ofHours( 1 ) ; // A one-hour appointment.
While we often want to adjust into a time zone for presentation to a user, generally behind-the-scenes it is best practice to track moments in UTC. So the starting and stopping points of an appointment calculated as moments should be done as a pair of Instant
objects.
Instant start = ldt.atZone( z ).toInstant() ;
Instant stop = start.plus( d ) ;
Interval
We can leverage a class to represent this pair of Instant
objects, Interval
.
This class is found in the ThreeTen-Extra library, a project led by the same man than led the Joda-Time, JSR 310, and java.time projects, Stephen Colebourne.
This class has very handy methods for comparison such as abuts
, overlaps
, contains
, and so on. You will likely want to use these methods in a scheduling app.
Appointment.java
Put this all together and we get a class like this:
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Might add some getter/setter methods in here.
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
}
When you generate a Interval
by calling your toInterval
method, you may want the individual start and stop moments.
Instant start = interval.getStart() ;
Instant stop = interval.getEnd() ;
Those two Instant
objects are in UTC by definition. If you want to see them through the lens of the wall-clock time used by the people of a particular region, apply a ZoneId
to get a ZonedDateTime
object.
ZoneId zAuckland = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdtStart = start.atZone( z ) ; // Adjust from UTC to some time zone. Same moment, same point on the timeline, different wall-clock time.
ZonedDateTime zdtStop = stop.atZone( z ) ;
Future
You asked about checking to see if this appointment is in the future. Again, we need a time zone to properly answer than. The time zones around the world currently cover a range of about 26 to 27 hours. So within in that many hours of the current moment we cannot tell if a LocalDateTime
is in the future or past without considering a time zone.
So let's add a method testing for the future that requires passing a time zone.
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5.");
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture ;
}
Start/Stop moments
Continuing the same theme on dynamically determining moments, let's add some methods to return the starting moment (inclusive) and stopping moment (exclusive). As discussed above, this requires passing a time zone.
The calling programmer could do this work herself. But I suspect this might be frequently needed enough to warrant adding these methods as a convenience.
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
Notice that I did not name these methods with the get…
. Accessor methods, getters & setters, by convention imply accessing a simple property stored within the object. But here we are not storing the ZonedDateTime
objects. These are dynamically-determined, so using a get…
method could be misleading. Instead, I am trying to follow the naming conventions laid down in the java.time project.
Immutable objects
Another lesson to learn from the java.time project is the immutable objects pattern.
Certain kinds of classes lend themselves to being read-only, created but not modified. The java.time classes certainly qualify. Whereas an invoice, for example, is expected to “mutate” (change), intuitively as a programmer I do not expect the date on the invoice to change unless I explicitly replace the date with a new object. So I want invoice to be a mutable object, but I want the LocalDate
object stored on that invoice to be immutable.
I suspect our Appointment
class might also be best designed an an immutable. So we have no setter
methods involved. To effectively alter an appointment in your scheduling app, create a new Appointment
object based on some of the values of the existing Appointment
object. Notice in the java.time classes how this is done with various with
methods, where the methods return a new object based on the original’s values but with some alteration.
Appointment.java
version 2
Let's put all that together into one example class.
And let's add a main
method to exercise this class. First we create one appointment, and look at its dynamically-determined moments in UTC. Second, we collect some Appointment
objects in a collection.
We have added a toString
method override to report on the object’s state.
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// An example class to show date-time handling for future appointments.
// Not necessarily ready for production use. Use at your own risk.
// Methods named according to the java.time naming conventions:
// https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to get the start/stop interval of an appointment. Message # bbf021e6-baa7-468d-83ad-cf73acb6702e." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture;
}
// -----------| Object overrides |---------------------------
@Override
public String toString ( ) {
return "Appointment{ " +
"start=" + start +
" | duration=" + duration +
" }";
}
// -----------| main |-------------
public static void main ( String args ) {
// See if a new appointment is in the future.
Appointment a = new Appointment( LocalDateTime.of( 2018 , 12 , 25 , 0 , 0 , 0 , 0 ) , Duration.ofHours( 2 ) );
ZoneId z = ZoneId.of( "America/Montreal" );
System.out.println( "For time zone: " + z + ", appointment interval is: " + a.toInterval( z ) );
System.out.println( "Start: " + a.toStartMoment( z ) );
System.out.println( "Stop: " + a.toStopMoment( z ) );
Boolean isFuture = a.isFuture( z );
System.out.println( a.toString() + " is future t/f: " + isFuture );
// Collect some appointments.
List < Appointment > list = new ArrayList <>( 3 );
list.add( a );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 13 , 15 , 0 , 0 , 0 ) , Duration.ofMinutes( 90 ) ) );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 30 , 16 , 0 , 0 , 0 ) , Duration.ofHours( 1 ) ) );
System.out.println( list );
}
}
When run.
For time zone: America/Montreal, appointment interval is: 2018-12-25T05:00:00Z/2018-12-25T07:00:00Z
Start: 2018-12-25T00:00-05:00[America/Montreal]
Stop: 2018-12-25T02:00-05:00[America/Montreal]
Appointment{ start=2018-12-25T00:00 | duration=PT2H } is future t/f: true
[Appointment{ start=2018-12-25T00:00 | duration=PT2H }, Appointment{ start=2018-12-13T15:00 | duration=PT1H30M }, Appointment{ start=2018-12-30T16:00 | duration=PT1H }]
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
- Later versions of Android bundle implementations of the java.time classes.
- For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.
1
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
add a comment |
Static block (almost)
Your code to exercise the class is in the wrong place. You have it stuck at the top of the class which is not syntactically correct. We can run code at the top, as a static block, but it needs to be labeled static { … }
. A static block is not commonly used in my experience. And certainly is not the right place for what you are doing there.
main
method
Instead you should be using a main
method. This non-OOP little thingie is a trick, a hack, to solve the chicken-or-the-egg conundrum, to get us from no-app-running to our OOP idea of heaven with a bunch of objects floating around and passing messages to one another.
When first learning Java, do not try to understand all the syntax and purpose of the main
method. Just accept it as a necessary evil to get the app running, it is merely the entrance point to execute the app. Focus on learning the OOP concepts and practices. Later, the main
method and syntax will make more sense.
Accessors
Here is a simplified re-write of your example code. We are just using a LocalDate
for simplicity, just enough to show (a) a main
method, and (b) getter/setter accessor methods.
package com.basilbourque.example;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AppointmentDate {
private LocalDate localDate;
// Constructor
public AppointmentDate ( LocalDate localDate ) {
this.localDate = localDate;
}
public LocalDate getLocalDate ( ) {
return localDate;
}
public void setLocalDate ( LocalDate localDate ) {
this.localDate = localDate;
}
@Override
public String toString ( ) {
return "AppointmentDate{ " +
"localDate=" + localDate +
" }";
}
// Not really a part of this class. A `main` method is just a hack to get our app launched.
public static void main ( String args ) {
String input = "23/01/2018";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" );
LocalDate ld = LocalDate.parse( input , f );
AppointmentDate ad = new AppointmentDate( ld );
ad.setLocalDate( ld.plusWeeks( 1 ) );
LocalDate newValue = ad.getLocalDate();
System.out.println( newValue.toString() ); // Generate text representing the value of this `LocalDate` object in standard ISO 8601 format.
List < AppointmentDate > list = new ArrayList <>( 3 );
list.add( ad );
list.add( new AppointmentDate( LocalDate.parse( "2018-02-13" ) ) );
list.add( new AppointmentDate( LocalDate.parse( "2018-12-21" ) ) );
System.out.println( list );
}
}
2018-01-30
[AppointmentDate{ localDate=2018-01-30 }, AppointmentDate{ localDate=2018-02-13 }, AppointmentDate{ localDate=2018-12-21 }]
java.time
You are using terrible old date-time classes that were supplanted years ago by the java.time classes. Never use Date
, Calendar
, SimpleDateFormat
, etc.
Appointments are tricky
While appointment tracking may seem intuitively simple, you are actually working on a very tricky subject.
The core problem is that politicians around the world are fond of redefining the time zone(s) under their jurisdiction. They do so quite frequently. They do so in both times of relative quiet and in times of turmoil.
The US and Canada have changed their offsets multiple times in recent decades. Turkey and Russia have changed their minds about going on or off DST multiple times in the last several years.
And politicians change their time zones with very little advance notice. And the notice seems to be growing shorter, despite the increased disturbance this causes in ever-more-computerized societies. Just last month, Morocco announced their country would stay on Daylight Saving Time (DST) permanent, cancelling on a Friday the DST cutover scheduled for that Sunday, leaving 0 business days warning — what a mess for IT staff. In May this year, North Korea slipped their clock a half-hour to sync with South Korea, with apparently immediate effect (no advance notice at all).
These frequent and unpredictable changes mean we cannot responsibly track future appointments as moments, as specific points on the timeline. When we say something like “3 PM on January 23rd” we usually mean 3 PM after politicians may have made their changes to the clock.
So we must store future appointments as a date and a time-of-day without a time zone or offset-from-UTC. Then, when calculating a calendar we must dynamically apply the rules for the intended time zone as currently defined on that day. If we perform this dynamic-determination of a moment today, and again in three days, if the politicians have announced a change in the time zone definition, and if we have been able to update our tzdata
data files in our operating systems, database engines, Java Virtual Machines, and various libraries, then we will arrive at a different moment in time.
LocalDateTime
The Local…
types in Java purposely lack any concept of time zone or offset-from-UTC. So they cannot represent a moment. So we never use these to pinpoint actual events that happened in the past. But these types are what we need for future appointments.
The LocalDateTime
class represents a date with a time-of-day without any zone/offset.
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
LocalTime lt = LocalTime.of( 15 , 0 ) ; // 3 PM in 24-hour time.
LocalDateTime ldt= LocalDateTime.of( ld , lt ) ;
ZonedDateTime
When calculating a calendar, when we need a specific moment, we apply a time zone (ZoneId
) to get a ZonedDateTime
object.
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ; // Determine a moment, a specific point on the timeline.
Instant
We can view that same moment in UTC by extracting a Instant
.
Instant instant = zdt.toInstant() ; // Adjust to UTC.
Duration
Appointments are generally best stored as a starting point plus a duration. No need to store the stopping point as that can be calculated.
Duration d = Duration.ofHours( 1 ) ; // A one-hour appointment.
While we often want to adjust into a time zone for presentation to a user, generally behind-the-scenes it is best practice to track moments in UTC. So the starting and stopping points of an appointment calculated as moments should be done as a pair of Instant
objects.
Instant start = ldt.atZone( z ).toInstant() ;
Instant stop = start.plus( d ) ;
Interval
We can leverage a class to represent this pair of Instant
objects, Interval
.
This class is found in the ThreeTen-Extra library, a project led by the same man than led the Joda-Time, JSR 310, and java.time projects, Stephen Colebourne.
This class has very handy methods for comparison such as abuts
, overlaps
, contains
, and so on. You will likely want to use these methods in a scheduling app.
Appointment.java
Put this all together and we get a class like this:
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Might add some getter/setter methods in here.
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
}
When you generate a Interval
by calling your toInterval
method, you may want the individual start and stop moments.
Instant start = interval.getStart() ;
Instant stop = interval.getEnd() ;
Those two Instant
objects are in UTC by definition. If you want to see them through the lens of the wall-clock time used by the people of a particular region, apply a ZoneId
to get a ZonedDateTime
object.
ZoneId zAuckland = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdtStart = start.atZone( z ) ; // Adjust from UTC to some time zone. Same moment, same point on the timeline, different wall-clock time.
ZonedDateTime zdtStop = stop.atZone( z ) ;
Future
You asked about checking to see if this appointment is in the future. Again, we need a time zone to properly answer than. The time zones around the world currently cover a range of about 26 to 27 hours. So within in that many hours of the current moment we cannot tell if a LocalDateTime
is in the future or past without considering a time zone.
So let's add a method testing for the future that requires passing a time zone.
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5.");
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture ;
}
Start/Stop moments
Continuing the same theme on dynamically determining moments, let's add some methods to return the starting moment (inclusive) and stopping moment (exclusive). As discussed above, this requires passing a time zone.
The calling programmer could do this work herself. But I suspect this might be frequently needed enough to warrant adding these methods as a convenience.
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
Notice that I did not name these methods with the get…
. Accessor methods, getters & setters, by convention imply accessing a simple property stored within the object. But here we are not storing the ZonedDateTime
objects. These are dynamically-determined, so using a get…
method could be misleading. Instead, I am trying to follow the naming conventions laid down in the java.time project.
Immutable objects
Another lesson to learn from the java.time project is the immutable objects pattern.
Certain kinds of classes lend themselves to being read-only, created but not modified. The java.time classes certainly qualify. Whereas an invoice, for example, is expected to “mutate” (change), intuitively as a programmer I do not expect the date on the invoice to change unless I explicitly replace the date with a new object. So I want invoice to be a mutable object, but I want the LocalDate
object stored on that invoice to be immutable.
I suspect our Appointment
class might also be best designed an an immutable. So we have no setter
methods involved. To effectively alter an appointment in your scheduling app, create a new Appointment
object based on some of the values of the existing Appointment
object. Notice in the java.time classes how this is done with various with
methods, where the methods return a new object based on the original’s values but with some alteration.
Appointment.java
version 2
Let's put all that together into one example class.
And let's add a main
method to exercise this class. First we create one appointment, and look at its dynamically-determined moments in UTC. Second, we collect some Appointment
objects in a collection.
We have added a toString
method override to report on the object’s state.
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// An example class to show date-time handling for future appointments.
// Not necessarily ready for production use. Use at your own risk.
// Methods named according to the java.time naming conventions:
// https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to get the start/stop interval of an appointment. Message # bbf021e6-baa7-468d-83ad-cf73acb6702e." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture;
}
// -----------| Object overrides |---------------------------
@Override
public String toString ( ) {
return "Appointment{ " +
"start=" + start +
" | duration=" + duration +
" }";
}
// -----------| main |-------------
public static void main ( String args ) {
// See if a new appointment is in the future.
Appointment a = new Appointment( LocalDateTime.of( 2018 , 12 , 25 , 0 , 0 , 0 , 0 ) , Duration.ofHours( 2 ) );
ZoneId z = ZoneId.of( "America/Montreal" );
System.out.println( "For time zone: " + z + ", appointment interval is: " + a.toInterval( z ) );
System.out.println( "Start: " + a.toStartMoment( z ) );
System.out.println( "Stop: " + a.toStopMoment( z ) );
Boolean isFuture = a.isFuture( z );
System.out.println( a.toString() + " is future t/f: " + isFuture );
// Collect some appointments.
List < Appointment > list = new ArrayList <>( 3 );
list.add( a );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 13 , 15 , 0 , 0 , 0 ) , Duration.ofMinutes( 90 ) ) );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 30 , 16 , 0 , 0 , 0 ) , Duration.ofHours( 1 ) ) );
System.out.println( list );
}
}
When run.
For time zone: America/Montreal, appointment interval is: 2018-12-25T05:00:00Z/2018-12-25T07:00:00Z
Start: 2018-12-25T00:00-05:00[America/Montreal]
Stop: 2018-12-25T02:00-05:00[America/Montreal]
Appointment{ start=2018-12-25T00:00 | duration=PT2H } is future t/f: true
[Appointment{ start=2018-12-25T00:00 | duration=PT2H }, Appointment{ start=2018-12-13T15:00 | duration=PT1H30M }, Appointment{ start=2018-12-30T16:00 | duration=PT1H }]
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
- Later versions of Android bundle implementations of the java.time classes.
- For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.
1
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
add a comment |
Static block (almost)
Your code to exercise the class is in the wrong place. You have it stuck at the top of the class which is not syntactically correct. We can run code at the top, as a static block, but it needs to be labeled static { … }
. A static block is not commonly used in my experience. And certainly is not the right place for what you are doing there.
main
method
Instead you should be using a main
method. This non-OOP little thingie is a trick, a hack, to solve the chicken-or-the-egg conundrum, to get us from no-app-running to our OOP idea of heaven with a bunch of objects floating around and passing messages to one another.
When first learning Java, do not try to understand all the syntax and purpose of the main
method. Just accept it as a necessary evil to get the app running, it is merely the entrance point to execute the app. Focus on learning the OOP concepts and practices. Later, the main
method and syntax will make more sense.
Accessors
Here is a simplified re-write of your example code. We are just using a LocalDate
for simplicity, just enough to show (a) a main
method, and (b) getter/setter accessor methods.
package com.basilbourque.example;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AppointmentDate {
private LocalDate localDate;
// Constructor
public AppointmentDate ( LocalDate localDate ) {
this.localDate = localDate;
}
public LocalDate getLocalDate ( ) {
return localDate;
}
public void setLocalDate ( LocalDate localDate ) {
this.localDate = localDate;
}
@Override
public String toString ( ) {
return "AppointmentDate{ " +
"localDate=" + localDate +
" }";
}
// Not really a part of this class. A `main` method is just a hack to get our app launched.
public static void main ( String args ) {
String input = "23/01/2018";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" );
LocalDate ld = LocalDate.parse( input , f );
AppointmentDate ad = new AppointmentDate( ld );
ad.setLocalDate( ld.plusWeeks( 1 ) );
LocalDate newValue = ad.getLocalDate();
System.out.println( newValue.toString() ); // Generate text representing the value of this `LocalDate` object in standard ISO 8601 format.
List < AppointmentDate > list = new ArrayList <>( 3 );
list.add( ad );
list.add( new AppointmentDate( LocalDate.parse( "2018-02-13" ) ) );
list.add( new AppointmentDate( LocalDate.parse( "2018-12-21" ) ) );
System.out.println( list );
}
}
2018-01-30
[AppointmentDate{ localDate=2018-01-30 }, AppointmentDate{ localDate=2018-02-13 }, AppointmentDate{ localDate=2018-12-21 }]
java.time
You are using terrible old date-time classes that were supplanted years ago by the java.time classes. Never use Date
, Calendar
, SimpleDateFormat
, etc.
Appointments are tricky
While appointment tracking may seem intuitively simple, you are actually working on a very tricky subject.
The core problem is that politicians around the world are fond of redefining the time zone(s) under their jurisdiction. They do so quite frequently. They do so in both times of relative quiet and in times of turmoil.
The US and Canada have changed their offsets multiple times in recent decades. Turkey and Russia have changed their minds about going on or off DST multiple times in the last several years.
And politicians change their time zones with very little advance notice. And the notice seems to be growing shorter, despite the increased disturbance this causes in ever-more-computerized societies. Just last month, Morocco announced their country would stay on Daylight Saving Time (DST) permanent, cancelling on a Friday the DST cutover scheduled for that Sunday, leaving 0 business days warning — what a mess for IT staff. In May this year, North Korea slipped their clock a half-hour to sync with South Korea, with apparently immediate effect (no advance notice at all).
These frequent and unpredictable changes mean we cannot responsibly track future appointments as moments, as specific points on the timeline. When we say something like “3 PM on January 23rd” we usually mean 3 PM after politicians may have made their changes to the clock.
So we must store future appointments as a date and a time-of-day without a time zone or offset-from-UTC. Then, when calculating a calendar we must dynamically apply the rules for the intended time zone as currently defined on that day. If we perform this dynamic-determination of a moment today, and again in three days, if the politicians have announced a change in the time zone definition, and if we have been able to update our tzdata
data files in our operating systems, database engines, Java Virtual Machines, and various libraries, then we will arrive at a different moment in time.
LocalDateTime
The Local…
types in Java purposely lack any concept of time zone or offset-from-UTC. So they cannot represent a moment. So we never use these to pinpoint actual events that happened in the past. But these types are what we need for future appointments.
The LocalDateTime
class represents a date with a time-of-day without any zone/offset.
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
LocalTime lt = LocalTime.of( 15 , 0 ) ; // 3 PM in 24-hour time.
LocalDateTime ldt= LocalDateTime.of( ld , lt ) ;
ZonedDateTime
When calculating a calendar, when we need a specific moment, we apply a time zone (ZoneId
) to get a ZonedDateTime
object.
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ; // Determine a moment, a specific point on the timeline.
Instant
We can view that same moment in UTC by extracting a Instant
.
Instant instant = zdt.toInstant() ; // Adjust to UTC.
Duration
Appointments are generally best stored as a starting point plus a duration. No need to store the stopping point as that can be calculated.
Duration d = Duration.ofHours( 1 ) ; // A one-hour appointment.
While we often want to adjust into a time zone for presentation to a user, generally behind-the-scenes it is best practice to track moments in UTC. So the starting and stopping points of an appointment calculated as moments should be done as a pair of Instant
objects.
Instant start = ldt.atZone( z ).toInstant() ;
Instant stop = start.plus( d ) ;
Interval
We can leverage a class to represent this pair of Instant
objects, Interval
.
This class is found in the ThreeTen-Extra library, a project led by the same man than led the Joda-Time, JSR 310, and java.time projects, Stephen Colebourne.
This class has very handy methods for comparison such as abuts
, overlaps
, contains
, and so on. You will likely want to use these methods in a scheduling app.
Appointment.java
Put this all together and we get a class like this:
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Might add some getter/setter methods in here.
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
}
When you generate a Interval
by calling your toInterval
method, you may want the individual start and stop moments.
Instant start = interval.getStart() ;
Instant stop = interval.getEnd() ;
Those two Instant
objects are in UTC by definition. If you want to see them through the lens of the wall-clock time used by the people of a particular region, apply a ZoneId
to get a ZonedDateTime
object.
ZoneId zAuckland = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdtStart = start.atZone( z ) ; // Adjust from UTC to some time zone. Same moment, same point on the timeline, different wall-clock time.
ZonedDateTime zdtStop = stop.atZone( z ) ;
Future
You asked about checking to see if this appointment is in the future. Again, we need a time zone to properly answer than. The time zones around the world currently cover a range of about 26 to 27 hours. So within in that many hours of the current moment we cannot tell if a LocalDateTime
is in the future or past without considering a time zone.
So let's add a method testing for the future that requires passing a time zone.
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5.");
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture ;
}
Start/Stop moments
Continuing the same theme on dynamically determining moments, let's add some methods to return the starting moment (inclusive) and stopping moment (exclusive). As discussed above, this requires passing a time zone.
The calling programmer could do this work herself. But I suspect this might be frequently needed enough to warrant adding these methods as a convenience.
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
Notice that I did not name these methods with the get…
. Accessor methods, getters & setters, by convention imply accessing a simple property stored within the object. But here we are not storing the ZonedDateTime
objects. These are dynamically-determined, so using a get…
method could be misleading. Instead, I am trying to follow the naming conventions laid down in the java.time project.
Immutable objects
Another lesson to learn from the java.time project is the immutable objects pattern.
Certain kinds of classes lend themselves to being read-only, created but not modified. The java.time classes certainly qualify. Whereas an invoice, for example, is expected to “mutate” (change), intuitively as a programmer I do not expect the date on the invoice to change unless I explicitly replace the date with a new object. So I want invoice to be a mutable object, but I want the LocalDate
object stored on that invoice to be immutable.
I suspect our Appointment
class might also be best designed an an immutable. So we have no setter
methods involved. To effectively alter an appointment in your scheduling app, create a new Appointment
object based on some of the values of the existing Appointment
object. Notice in the java.time classes how this is done with various with
methods, where the methods return a new object based on the original’s values but with some alteration.
Appointment.java
version 2
Let's put all that together into one example class.
And let's add a main
method to exercise this class. First we create one appointment, and look at its dynamically-determined moments in UTC. Second, we collect some Appointment
objects in a collection.
We have added a toString
method override to report on the object’s state.
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// An example class to show date-time handling for future appointments.
// Not necessarily ready for production use. Use at your own risk.
// Methods named according to the java.time naming conventions:
// https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to get the start/stop interval of an appointment. Message # bbf021e6-baa7-468d-83ad-cf73acb6702e." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture;
}
// -----------| Object overrides |---------------------------
@Override
public String toString ( ) {
return "Appointment{ " +
"start=" + start +
" | duration=" + duration +
" }";
}
// -----------| main |-------------
public static void main ( String args ) {
// See if a new appointment is in the future.
Appointment a = new Appointment( LocalDateTime.of( 2018 , 12 , 25 , 0 , 0 , 0 , 0 ) , Duration.ofHours( 2 ) );
ZoneId z = ZoneId.of( "America/Montreal" );
System.out.println( "For time zone: " + z + ", appointment interval is: " + a.toInterval( z ) );
System.out.println( "Start: " + a.toStartMoment( z ) );
System.out.println( "Stop: " + a.toStopMoment( z ) );
Boolean isFuture = a.isFuture( z );
System.out.println( a.toString() + " is future t/f: " + isFuture );
// Collect some appointments.
List < Appointment > list = new ArrayList <>( 3 );
list.add( a );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 13 , 15 , 0 , 0 , 0 ) , Duration.ofMinutes( 90 ) ) );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 30 , 16 , 0 , 0 , 0 ) , Duration.ofHours( 1 ) ) );
System.out.println( list );
}
}
When run.
For time zone: America/Montreal, appointment interval is: 2018-12-25T05:00:00Z/2018-12-25T07:00:00Z
Start: 2018-12-25T00:00-05:00[America/Montreal]
Stop: 2018-12-25T02:00-05:00[America/Montreal]
Appointment{ start=2018-12-25T00:00 | duration=PT2H } is future t/f: true
[Appointment{ start=2018-12-25T00:00 | duration=PT2H }, Appointment{ start=2018-12-13T15:00 | duration=PT1H30M }, Appointment{ start=2018-12-30T16:00 | duration=PT1H }]
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
- Later versions of Android bundle implementations of the java.time classes.
- For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.
Static block (almost)
Your code to exercise the class is in the wrong place. You have it stuck at the top of the class which is not syntactically correct. We can run code at the top, as a static block, but it needs to be labeled static { … }
. A static block is not commonly used in my experience. And certainly is not the right place for what you are doing there.
main
method
Instead you should be using a main
method. This non-OOP little thingie is a trick, a hack, to solve the chicken-or-the-egg conundrum, to get us from no-app-running to our OOP idea of heaven with a bunch of objects floating around and passing messages to one another.
When first learning Java, do not try to understand all the syntax and purpose of the main
method. Just accept it as a necessary evil to get the app running, it is merely the entrance point to execute the app. Focus on learning the OOP concepts and practices. Later, the main
method and syntax will make more sense.
Accessors
Here is a simplified re-write of your example code. We are just using a LocalDate
for simplicity, just enough to show (a) a main
method, and (b) getter/setter accessor methods.
package com.basilbourque.example;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AppointmentDate {
private LocalDate localDate;
// Constructor
public AppointmentDate ( LocalDate localDate ) {
this.localDate = localDate;
}
public LocalDate getLocalDate ( ) {
return localDate;
}
public void setLocalDate ( LocalDate localDate ) {
this.localDate = localDate;
}
@Override
public String toString ( ) {
return "AppointmentDate{ " +
"localDate=" + localDate +
" }";
}
// Not really a part of this class. A `main` method is just a hack to get our app launched.
public static void main ( String args ) {
String input = "23/01/2018";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu" );
LocalDate ld = LocalDate.parse( input , f );
AppointmentDate ad = new AppointmentDate( ld );
ad.setLocalDate( ld.plusWeeks( 1 ) );
LocalDate newValue = ad.getLocalDate();
System.out.println( newValue.toString() ); // Generate text representing the value of this `LocalDate` object in standard ISO 8601 format.
List < AppointmentDate > list = new ArrayList <>( 3 );
list.add( ad );
list.add( new AppointmentDate( LocalDate.parse( "2018-02-13" ) ) );
list.add( new AppointmentDate( LocalDate.parse( "2018-12-21" ) ) );
System.out.println( list );
}
}
2018-01-30
[AppointmentDate{ localDate=2018-01-30 }, AppointmentDate{ localDate=2018-02-13 }, AppointmentDate{ localDate=2018-12-21 }]
java.time
You are using terrible old date-time classes that were supplanted years ago by the java.time classes. Never use Date
, Calendar
, SimpleDateFormat
, etc.
Appointments are tricky
While appointment tracking may seem intuitively simple, you are actually working on a very tricky subject.
The core problem is that politicians around the world are fond of redefining the time zone(s) under their jurisdiction. They do so quite frequently. They do so in both times of relative quiet and in times of turmoil.
The US and Canada have changed their offsets multiple times in recent decades. Turkey and Russia have changed their minds about going on or off DST multiple times in the last several years.
And politicians change their time zones with very little advance notice. And the notice seems to be growing shorter, despite the increased disturbance this causes in ever-more-computerized societies. Just last month, Morocco announced their country would stay on Daylight Saving Time (DST) permanent, cancelling on a Friday the DST cutover scheduled for that Sunday, leaving 0 business days warning — what a mess for IT staff. In May this year, North Korea slipped their clock a half-hour to sync with South Korea, with apparently immediate effect (no advance notice at all).
These frequent and unpredictable changes mean we cannot responsibly track future appointments as moments, as specific points on the timeline. When we say something like “3 PM on January 23rd” we usually mean 3 PM after politicians may have made their changes to the clock.
So we must store future appointments as a date and a time-of-day without a time zone or offset-from-UTC. Then, when calculating a calendar we must dynamically apply the rules for the intended time zone as currently defined on that day. If we perform this dynamic-determination of a moment today, and again in three days, if the politicians have announced a change in the time zone definition, and if we have been able to update our tzdata
data files in our operating systems, database engines, Java Virtual Machines, and various libraries, then we will arrive at a different moment in time.
LocalDateTime
The Local…
types in Java purposely lack any concept of time zone or offset-from-UTC. So they cannot represent a moment. So we never use these to pinpoint actual events that happened in the past. But these types are what we need for future appointments.
The LocalDateTime
class represents a date with a time-of-day without any zone/offset.
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;
LocalTime lt = LocalTime.of( 15 , 0 ) ; // 3 PM in 24-hour time.
LocalDateTime ldt= LocalDateTime.of( ld , lt ) ;
ZonedDateTime
When calculating a calendar, when we need a specific moment, we apply a time zone (ZoneId
) to get a ZonedDateTime
object.
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ; // Determine a moment, a specific point on the timeline.
Instant
We can view that same moment in UTC by extracting a Instant
.
Instant instant = zdt.toInstant() ; // Adjust to UTC.
Duration
Appointments are generally best stored as a starting point plus a duration. No need to store the stopping point as that can be calculated.
Duration d = Duration.ofHours( 1 ) ; // A one-hour appointment.
While we often want to adjust into a time zone for presentation to a user, generally behind-the-scenes it is best practice to track moments in UTC. So the starting and stopping points of an appointment calculated as moments should be done as a pair of Instant
objects.
Instant start = ldt.atZone( z ).toInstant() ;
Instant stop = start.plus( d ) ;
Interval
We can leverage a class to represent this pair of Instant
objects, Interval
.
This class is found in the ThreeTen-Extra library, a project led by the same man than led the Joda-Time, JSR 310, and java.time projects, Stephen Colebourne.
This class has very handy methods for comparison such as abuts
, overlaps
, contains
, and so on. You will likely want to use these methods in a scheduling app.
Appointment.java
Put this all together and we get a class like this:
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Might add some getter/setter methods in here.
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
}
When you generate a Interval
by calling your toInterval
method, you may want the individual start and stop moments.
Instant start = interval.getStart() ;
Instant stop = interval.getEnd() ;
Those two Instant
objects are in UTC by definition. If you want to see them through the lens of the wall-clock time used by the people of a particular region, apply a ZoneId
to get a ZonedDateTime
object.
ZoneId zAuckland = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdtStart = start.atZone( z ) ; // Adjust from UTC to some time zone. Same moment, same point on the timeline, different wall-clock time.
ZonedDateTime zdtStop = stop.atZone( z ) ;
Future
You asked about checking to see if this appointment is in the future. Again, we need a time zone to properly answer than. The time zones around the world currently cover a range of about 26 to 27 hours. So within in that many hours of the current moment we cannot tell if a LocalDateTime
is in the future or past without considering a time zone.
So let's add a method testing for the future that requires passing a time zone.
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5.");
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture ;
}
Start/Stop moments
Continuing the same theme on dynamically determining moments, let's add some methods to return the starting moment (inclusive) and stopping moment (exclusive). As discussed above, this requires passing a time zone.
The calling programmer could do this work herself. But I suspect this might be frequently needed enough to warrant adding these methods as a convenience.
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
Notice that I did not name these methods with the get…
. Accessor methods, getters & setters, by convention imply accessing a simple property stored within the object. But here we are not storing the ZonedDateTime
objects. These are dynamically-determined, so using a get…
method could be misleading. Instead, I am trying to follow the naming conventions laid down in the java.time project.
Immutable objects
Another lesson to learn from the java.time project is the immutable objects pattern.
Certain kinds of classes lend themselves to being read-only, created but not modified. The java.time classes certainly qualify. Whereas an invoice, for example, is expected to “mutate” (change), intuitively as a programmer I do not expect the date on the invoice to change unless I explicitly replace the date with a new object. So I want invoice to be a mutable object, but I want the LocalDate
object stored on that invoice to be immutable.
I suspect our Appointment
class might also be best designed an an immutable. So we have no setter
methods involved. To effectively alter an appointment in your scheduling app, create a new Appointment
object based on some of the values of the existing Appointment
object. Notice in the java.time classes how this is done with various with
methods, where the methods return a new object based on the original’s values but with some alteration.
Appointment.java
version 2
Let's put all that together into one example class.
And let's add a main
method to exercise this class. First we create one appointment, and look at its dynamically-determined moments in UTC. Second, we collect some Appointment
objects in a collection.
We have added a toString
method override to report on the object’s state.
package com.basilbourque.example;
import org.threeten.extra.Interval;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
// An example class to show date-time handling for future appointments.
// Not necessarily ready for production use. Use at your own risk.
// Methods named according to the java.time naming conventions:
// https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html
public class Appointment {
private LocalDateTime start;
private Duration duration;
// Constructor.
public Appointment ( LocalDateTime start , Duration duration ) {
this.start = start;
this.duration = duration;
}
// Dynamically determine the start and stop points of this appointment, given today’s definition of the intended time zone.
public Interval toInterval ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to get the start/stop interval of an appointment. Message # bbf021e6-baa7-468d-83ad-cf73acb6702e." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
Interval interval = Interval.of( zdtStart.toInstant() , this.duration );
return interval;
}
// Get start moment for a particular time zone.
public ZonedDateTime toStartMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getStart().atZone( zoneId );
return zdt;
}
// Get stop moment for a particular time zone.
public ZonedDateTime toStopMoment ( ZoneId zoneId ) {
ZonedDateTime zdt = this.toInterval( zoneId ).getEnd().atZone( zoneId );
return zdt;
}
// Dynamically determine if this appointment will be in the future for some specific time zone.
public Boolean isFuture ( ZoneId zoneId ) {
Objects.requireNonNull( zoneId , "Must pass a time zone to determine if an appointment is in the future. Message # e1c64bc1-9a44-4d15-b20d-e68414fb5ab5." );
ZonedDateTime zdtStart = this.start.atZone( zoneId );
ZonedDateTime zdtNow = ZonedDateTime.now( zoneId );
boolean isInTheFuture = zdtNow.isBefore( zdtStart );
return isInTheFuture;
}
// -----------| Object overrides |---------------------------
@Override
public String toString ( ) {
return "Appointment{ " +
"start=" + start +
" | duration=" + duration +
" }";
}
// -----------| main |-------------
public static void main ( String args ) {
// See if a new appointment is in the future.
Appointment a = new Appointment( LocalDateTime.of( 2018 , 12 , 25 , 0 , 0 , 0 , 0 ) , Duration.ofHours( 2 ) );
ZoneId z = ZoneId.of( "America/Montreal" );
System.out.println( "For time zone: " + z + ", appointment interval is: " + a.toInterval( z ) );
System.out.println( "Start: " + a.toStartMoment( z ) );
System.out.println( "Stop: " + a.toStopMoment( z ) );
Boolean isFuture = a.isFuture( z );
System.out.println( a.toString() + " is future t/f: " + isFuture );
// Collect some appointments.
List < Appointment > list = new ArrayList <>( 3 );
list.add( a );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 13 , 15 , 0 , 0 , 0 ) , Duration.ofMinutes( 90 ) ) );
list.add( new Appointment( LocalDateTime.of( 2018 , 12 , 30 , 16 , 0 , 0 , 0 ) , Duration.ofHours( 1 ) ) );
System.out.println( list );
}
}
When run.
For time zone: America/Montreal, appointment interval is: 2018-12-25T05:00:00Z/2018-12-25T07:00:00Z
Start: 2018-12-25T00:00-05:00[America/Montreal]
Stop: 2018-12-25T02:00-05:00[America/Montreal]
Appointment{ start=2018-12-25T00:00 | duration=PT2H } is future t/f: true
[Appointment{ start=2018-12-25T00:00 | duration=PT2H }, Appointment{ start=2018-12-13T15:00 | duration=PT1H30M }, Appointment{ start=2018-12-30T16:00 | duration=PT1H }]
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date
, Calendar
, & SimpleDateFormat
.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.*
classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
- Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
- Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
- Later versions of Android bundle implementations of the java.time classes.
- For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval
, YearWeek
, YearQuarter
, and more.
edited Nov 21 '18 at 21:35
answered Nov 21 '18 at 4:14
Basil BourqueBasil Bourque
110k27377541
110k27377541
1
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
add a comment |
1
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
1
1
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
@OleV.V. Thanks, I clarified my thoughts there.
– Basil Bourque
Nov 21 '18 at 21:31
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53403042%2fappointment-class%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Why do you not just use
Date
as it is? Why want you to compare anAppointment
(containing start and end date) with a singleDate
object?– Michael Butscher
Nov 20 '18 at 23:23
1
Use LocalDate (or LocalDateTIme) it's easier ;)=
– azro
Nov 20 '18 at 23:26