12

I am sure this was done 1000 times in 1000 different places. The question is I want to know if there is a better/standard/faster way to check if current "time" is between two time values given in hh:mm:ss format. For example, my big business logic should not run between 18:00:00 and 18:30:00. So here is what I had in mind:

 public static  boolean isCurrentTimeBetween(String starthhmmss, String endhhmmss) throws ParseException{
  DateFormat hhmmssFormat = new SimpleDateFormat("yyyyMMddhh:mm:ss");
  Date now = new Date();
  String yyyMMdd = hhmmssFormat.format(now).substring(0, 8);

  return(hhmmssFormat.parse(yyyMMdd+starthhmmss).before(now) &&
    hhmmssFormat.parse(yyyMMdd+endhhmmss).after(now));
 }

Example test case:

  String doNotRunBetween="18:00:00,18:30:00";//read from props file
  String[] hhmmss = downTime.split(",");
  if(isCurrentTimeBetween(hhmmss[0], hhmmss[1])){
   System.out.println("NOT OK TO RUN");
  }else{
   System.out.println("OK TO RUN");
  }

What I am looking for is code that is better

  • in performance
  • in looks
  • in correctness

What I am not looking for

  • third-party libraries
  • Exception handling debate
  • variable naming conventions
  • method modifier issues
ring bearer
  • 18,848
  • 7
  • 53
  • 68
  • 5
    Not looking for third-party libraries excludes a lot of fanatic answers about Joda Time... – JuanZe Mar 18 '10 at 20:12
  • 1
    @skaffman Yes, Joda Time is great, but all I require for now is this one functionality related to time and date. – ring bearer Mar 18 '10 at 20:34
  • @JuanZe - how do you do it with Jodatime? I have the same requirement and can do it with joda time. I need to find out if a current hour is in between two given hours. – Andy Dufresne Jul 17 '14 at 06:23

6 Answers6

23

this is all you should need to do, this method is loosely coupled from the input and highly coherent.

boolean isNowBetweenDateTime(final Date s, final Date e)
{
    final Date now = new Date();
    return now.after(s) && now.before(e);
}

how you get the Date objects for start and end is irrelevant to comparing them. You are making things way more complicated than you need to with passing String representations around.

Here is a better way to get the start and end dates, again loosely coupled and highly coherent.

private Date dateFromHourMinSec(final String hhmmss)
{
    if (hhmmss.matches("^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$"))
    {
        final String[] hms = hhmmss.split(":");
        final GregorianCalendar gc = new GregorianCalendar();
        gc.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hms[0]));
        gc.set(Calendar.MINUTE, Integer.parseInt(hms[1]));
        gc.set(Calendar.SECOND, Integer.parseInt(hms[2]));
        gc.set(Calendar.MILLISECOND, 0);
        return gc.getTime();
    }
    else
    {
        throw new IllegalArgumentException(hhmmss + " is not a valid time, expecting HH:MM:SS format");
    }
}

Now you can make two well named method calls that will be pretty self documenting.

  • @fuzzy lollipop You possibly overlooked at the fact that I am just getting time in format of "hh:mm:ss" as the input. That is why specifically named it "isCurrentTimeBetween" – ring bearer Mar 18 '10 at 20:19
  • 3
    how you get the data is irrelevant, it should all be normalized into a java.util.Date object so it can be leveraged. –  Mar 18 '10 at 20:26
  • Naming it a certain way doesn't make it not a bad design. Look up "loosely coupled" and "highly coherent". –  Mar 18 '10 at 20:33
  • I think I'd rather use the OP's SimpleDateFormat() to parse the String; why reinvent the wheel? But separating parsing from "between" determination may be useful. – Michael Campbell Mar 18 '10 at 22:33
  • he is going around the world with a bunch of string manipulation to do what the Calendar class is designed to do, he is re-inventing the wheel not the other way around. Plus my way doesn't make all the client code deal with that ParseException which he ommits from his example code. –  Mar 19 '10 at 00:19
4

As pointed out by Kevin, Fuzzy Lollipop's Regex won't pick up times between 14:00 and 19:00.

To get match a full 24 hour clock, you can use this:

if (hhmmss.matches("^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"))
{
    // Do stuff here
}
Pineechio
  • 373
  • 1
  • 9
4

tl;dr

LocalTime now = ZonedDateTime.now( ZoneId.of( "America/Montreal" ) )
                             .toLocalTime() ;
Boolean isBetween = ( ! now.isBefore( LocalTime.of( 18 , 0 ) )  // "not before" means "is equal to OR after".
                    && 
                    now.isBefore( LocalTime.of( 18 , 30 ) ) ;  // Half-Open, beginning is *inclusive* while ending is *exclusive*.

Using java.time

You are using old date-time classes that have proven to be poorly designed, confusing, and troublesome. They are now legacy, supplanted by the java.time classes.

LocalTime

Do not pass mere strings representing time-of-day values. We now have a type for that, the LocalTime class.

LocalTime start = LocalTime.of( 18 , 0 );
LocalTime stop = LocalTime.of( 18 , 30 );

Pass those instances to your utility method. That method should not have to do any parsing, so no need to throw the parsing exception.

public static  boolean isCurrentTimeBetween( LocalTime start , LocalTime stop ) {
…

ZonedDateTime

A time zone is crucial in determining the current date and time-of-day. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.now( z );

To compare the time-of-day of now, we could simply extract a LocalTime from that ZonedDateTime. But we have the problem of anomalies, such as Daylight Saving Time (DST) and politicians redefining time zones. There may not be any 6 PM hour on a particular date. The solution to this conundrum depends on your business context and your business rules. You could either ignore the conundrum and stick with literally asking if the current time is between your target start-stop time. Or you could apply the time zone to your start-stop times-of-day of day and let ZonedDateTime class make adjustments as it sees fit. Let's look at both approaches.

Ignore anomalies

First, ignore any anomalies. Ask simply and literally if the current time-of-day is between the target start and stop times-of-day.

We can extract a time-of-day object from the zoned date-time object.

LocalTime localTimeNow = zdt.toLocalTime(); // Extract a time-of-day from the zoned date-time object.

Compare that to our stop-start times-of-day. Note that we use here the Half-Open approach to defining a span of time. In this approach the beginning is inclusive while the ending is exclusive. This approach is common in date-time work and generally is the wise way to go.

Boolean isNowOnOrAfterStart = ( ! localTimeNow.isBefore( start ) ) ;  // A briefer way of asking "is equal to OR is after" is "is not before". 
Boolean isNowBeforeStop = localTimeNow.isBefore( stop );
Boolean isNowInTargetZone = ( isNowOnOrAfterStart && isNowBeforeStop ); // Half-Open: beginning is inclusive while ending is exclusive.

Consider anomalies

Next we consider any anomalies. We apply the start and stop times-of-day to the current date within the same time zone. We extract the date-only from the zoned date-time object.

LocalDate localDateToday = zdt.toLocalDate();
ZonedDateTime zdtStart = ZonedDateTime.of( localDateToday , start , z );
ZonedDateTime zdtStop = ZonedDateTime.of( localDateToday , stop , z );

Study the class documentation to understand the behavior of ZonedDateTime.of in resolving invalid time-of-day values. There is no perfect way to resolve nonexistent time-of-day values, so you must decide if this class’ way meets your business rules.

ZonedDateTime.of

public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone)

Obtains an instance of ZonedDateTime from a local date and time. This creates a zoned date-time matching the input local date and time as closely as possible. Time-zone rules, such as daylight savings, mean that not every local date-time is valid for the specified zone, thus the local date-time may be adjusted.

The local date time and first combined to form a local date-time. The local date-time is then resolved to a single instant on the time-line. This is achieved by finding a valid offset from UTC/Greenwich for the local date-time as defined by the rules of the zone ID.

In most cases, there is only one valid offset for a local date-time. In the case of an overlap, when clocks are set back, there are two valid offsets. This method uses the earlier offset typically corresponding to "summer".

In the case of a gap, when clocks jump forward, there is no valid offset. Instead, the local date-time is adjusted to be later by the length of the gap. For a typical one hour daylight savings change, the local date-time will be moved one hour later into the offset typically corresponding to "summer".

Apply the same comparison logic as we saw above.

Boolean isNowOnOrAfterStart = ( ! zdt.isBefore( zdtStart ) ) ;  // A briefer way of asking "is equal to OR is after" is "is not before". 
Boolean isNowBeforeStop = zdt.isBefore( zdtStop );
Boolean isNowInTargetZone = ( isNowOnOrAfterStart && isNowBeforeStop ); // Half-Open: beginning is inclusive while ending is exclusive.

Alternative way to make the comparison is to use the handy Interval class from the ThreeTen-Extra project. That class takes a pain of Instant objects, which you can extract from your ZonedDateTime objects. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

Interval interval = Interval.of( zdtStart.toInstant() , zdtStop.toInstant() );
Boolean isNowInTargetZone = interval.contains( zdt.toInstant() );

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.

Where to obtain the java.time classes?

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.

Community
  • 1
  • 1
Basil Bourque
  • 218,480
  • 72
  • 657
  • 915
1

The following Class is something I just created out of some of the code from other answers. It encapsulates the behavior of a 'time period' without relating to specific days. Our system is using this Class to check if the current time is within one of our designated maintenance windows. i.e. 05:00:00 - 07:00:00

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
*
* @author Adam Yocum
*/
public class ExclusionTimePeriod {
    private String timeStart;
    private String timeEnd;

    /**
    * @return the timeStart
    */
    public String getTimeStart() {
        return timeStart;
    }

    /**
    * @param timeStart the timeStart to set
    */
    public void setTimeStart(String timeStart) {
        if (timeStart.matches("^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"))
        {
            this.timeStart = timeStart;
        }
        else
        {
            throw new IllegalArgumentException(timeStart + " is not a valid time, expecting HH:MM:SS format");
        }

    }

    /**
    * @return the timeEnd
    */
    public String getTimeEnd() {
        return timeEnd;
    }

    /**
    * @param timeEnd the timeEnd to set
    */
    public void setTimeEnd(String timeEnd) {
        if (timeEnd.matches("^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"))
        {
            this.timeEnd = timeEnd;
        }
        else
        {
            throw new IllegalArgumentException(timeEnd + " is not a valid time, expecting HH:MM:SS format");
        }
    }

    private Date toDate(String hhmmss){
        final String[] hms = hhmmss.split(":");
        final GregorianCalendar gc = new GregorianCalendar();
        gc.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hms[0]));
        gc.set(Calendar.MINUTE, Integer.parseInt(hms[1]));
        gc.set(Calendar.SECOND, Integer.parseInt(hms[2]));
        gc.set(Calendar.MILLISECOND, 0);
        Date date = gc.getTime();
        return date;
    }

    public boolean isNowInPeriod()
    {
        final Date now = new Date();
        return now.after(toDate(getTimeStart())) && now.before(toDate(getTimeEnd()));
    }

    public static void main(String[] args){

        //Test All possible hours
        for(int hour=0;hour<=23;hour++){

            String hourStr = "";
            if(hour<=9){
                hourStr = "0"+hour;
            }else{
                hourStr = ""+hour;
            }

            for(int min=0;min<60;min++){
                String minStr = "";
                if(min<=9){
                    minStr = "0"+min;
                }else{
                    minStr = ""+min;
                }

                for(int sec=0;sec<60;sec++){
                    String secStr = "";
                    if(sec<=9){
                        secStr = "0"+sec;
                    }else{
                        secStr = ""+sec;
                    }

                    String hhmmss = hourStr+":"+minStr+":"+secStr;

                    ExclusionTimePeriod period = new ExclusionTimePeriod();
                    period.setTimeStart(hhmmss);
                    period.setTimeEnd(hhmmss);

                    System.out.println(hhmmss+" Ok");
                }
            }
        }


        //Test isInPeriod functionality
        ExclusionTimePeriod isInTest = new ExclusionTimePeriod();
        isInTest.setTimeStart("10:00:00");
        isInTest.setTimeEnd("10:43:00");

        System.out.println((new Date())+" is between "+isInTest.getTimeStart()+" and "+isInTest.getTimeEnd()+" = "+isInTest.isNowInPeriod());

    }
}
Adam Yocum
  • 11
  • 1
1

The Midnight Problem

Other answers fail to mention it - and the OP doesn't ask - but you should really consider when the interval spans across midnight.

Time is difficult. I purposely left the "long" version of the code and didn't abbreviate logical conditions to make it as clear as possible the what's and the why's.

/**
 * Takes into consideration that the interval may span accross midnight
 *
 * @param clock to make unit testing easier, just replace for Clock.systemUTC() in your code 
 * @param start the interval start
 * @param end the interval end
 * @return true if "now" is inside the specified interval
 */
static boolean isNowBetweenLocalTime(Clock clock, final LocalTime start, final LocalTime end) {
    LocalTime now = LocalTime.now(clock);

    // if interval crosses midnight
    if (end.isBefore(start)) {
        if (now.isAfter(start) && now.isAfter(end)) {
            return true;
        }
        if (now.isBefore(start) && now.isBefore(end)) {
            return true;
        }
        return false;
    }

    // if interval does not cross midnight
    if (end.isAfter(start)) {
        if (now.isAfter(start) && now.isBefore(end)) {
            return true;
        }
        return false;
    }

    return false; // interval is 0 so start and end always outside interval
}

Verbosity is not always wrong. This method will be buried in a utility class and two years from now you'll thank yourself for understanding what it does!

Frankie
  • 23,189
  • 10
  • 74
  • 112
0

The dateFromHourMinSec method is flawed as written. It won't allow any hours where the seconde digit is greater than 3, e.g. 18:00:00. If you change it to allow [0-2][0-9] it will allow times such as 29:00:00. Have a fix for that?

Kevin
  • 1
  • 1