3

I receive the below startdate and enddate as input in my program.

2017-03-07 06:30:00 to 2017-03-07 11:35:00 (yyyy-mm-dd hh:mi:ss)

I want to break the dates down by hour. For the example that I gave above , I want to create a list of values similar to what is listed below.

2017-03-07 0630-07 
2017-03-07 07-08  
2017-03-07 08-09 
2017-03-07 09-10  
2017-03-07 10-11  
2017-03-07 11-1135

Can you please suggest an approach that I can explore?

Punter Vicky
  • 15,954
  • 56
  • 188
  • 315
  • Sorry Aaron , I will exit my post and will explain it clearly. – Punter Vicky Mar 06 '17 at 16:02
  • Alright I get it now ! I'll try to see what I can find. – Aaron Mar 06 '17 at 16:05
  • Thanks Aaron , yes that's correct. I want to break them down by timespan. I haven't coded this yet. I wanted to check for recommended approach before implementing. – Punter Vicky Mar 06 '17 at 16:05
  • You can absolutely do this in Java, but often, when I'm doing this sort of stuff, it's a one-time job, and I use something like Python. – David Ehrmann Mar 06 '17 at 16:14
  • @DavidEhrmann , thank you. I am trying to get count of data from a specific table as part of reporting app. The query is complex is taking more than 2 minutes even after tuning as it has multiple conditions. So I am trying to cache the data by hour. If any request is coming through for data in the cache , then I would compute the count by adding up the count per hour from cache. I will query the db only for data which is not cached. – Punter Vicky Mar 06 '17 at 16:18
  • 1
    @PunterVicky Where does the Map mentioned in the title come into the picture? I suggest you either explain that more clearly, or edit your title to delete the mention if not really relevant to your core issue. – Basil Bourque Mar 06 '17 at 17:17
  • @BasilBourque thanks for highlighting , I edited the title. – Punter Vicky Mar 06 '17 at 17:26

3 Answers3

4

If you can use Java8, here's the approach I'd suggest :

// with LocalDateTime startDate and LocalDateTime endDate defined,
LocalDateTime currentRangeStart = startDate;
while (currentRangeStart.isBefore(endDate)) {
    LocalDateTime nextHour = currentRangeStart.withMinute(0).plusHours(1);
    LocalDateTime currentRangeEnd = nextHour.isBefore(endDate) ? nextHour : endDate;
    System.out.printf("%s - %s%n", currentRangeStart, currentRangeEnd);
    currentRangeStart = currentRangeEnd;
}

Here you can see it in action. If you currently have Date objects, check this question to see how to convert from Date to LocalDateTime.

Community
  • 1
  • 1
Aaron
  • 24,009
  • 2
  • 33
  • 57
2

I would build a map from the start-date to a count. Something like...

Map<java.util.Date,Integer> map = new TreeMap<>();
Calendar cal = new GregorianCalendar();
for (java.sql.Timestamp exactDate: dates) {
  cal.setTime(exactDate);
  cal.set(Calendar.MINUTE, 0);
  cal.set(Calendar.SECOND, 0);
  cal.set(Calendar.MILLISECOND, 0);
  java.util.Date key = new java.util.Date(cal.getTime());
  Integer count;
  if ((count = map.get(key)) == null) {
    map.put(key, Integer.valueOf(1);
  } else {
    map.put(key, Integer.valueOf(1 + count.intValue()));
  }
}

Then iterate over the map to pull out the values; or do a for loop over all possible values from the first to the last to print "zero" hours as well.

However, it's probably lest error-prone and more efficient to do the work of sorting and grouping in a database query, something like

SELECT trunc(log_date,'HH24'),count(*) 
FROM tbl
GROUP BY trunc(log_date,'HH24')
ORDER BY trunc(log_date,'HH24')
david
  • 997
  • 6
  • 15
1

The Answer by Aaron is good, but it ignores any seconds or fraction-of-second that may be present in the inputs. Also, I can expand a bit more, and apply time zone.

Truncate

You can truncate the LocalDateTime objects eliminate any whole or fractional seconds as you increment the hours.

LocalDateTime ldtTruncated = ldt.truncatedTo( ChronoUnit.MINUTES ); // Lop off seconds and nanoseconds.

…or…

LocalDateTime ldtTruncated = ldt.truncatedTo( ChronoUnit.HOURS ); // Lop off minutes, seconds, and nanoseconds.

Time zone

A crucial issue is time zone. These values lack any indicator of offset-from-UTC or time zone. So as correctly shown by Aaron, we should initially parse them as LocalDateTime.

LocalDateTime ldtStart = LocalDateTime.parse ( "2017-03-07 06:30:00".replace ( " " , "T" ) );
LocalDateTime ldtStop = LocalDateTime.parse ( "2017-03-07 11:35:00".replace ( " " , "T" ) );

But I assume from the wording of the Question that we want to work with specific moments. A LocalDateTime by definition is not a specific moment, only a vague idea of potential moments. You must add the context of a time zone (or offset) to determine actual moment. So if you know the zone intended for these values, apply it.

The ZonedDateTime class makes adjustments for input values that are invalid for your particular time zone because of anomalies such as Daylight Saving Time (DST).

ZoneId z = ZoneId.of ( "America/Montreal" );
ZonedDateTime zdtStart = ldtStart.atZone ( z );
ZonedDateTime zdtStop = ldtStop.atZone ( z );

Make sure the inputs make sense.

// Validate the inputs.
if ( zdtStop.isBefore ( zdtStart ) ) {
    // perhaps throw exception
    System.out.println ( "ERROR - stop is befor start." );
    return;
}

Date-time range as a pair of ZonedDateTime objects

Now we can increment hour-by-hour to generate your desired ranges. We could use the Interval class from the ThreeTen-Extra project (see below) which track a pair of Instant objects which are always in UTC. But that may not be useful for us as we really want to work with the zoned hours (or so I assume). So instead let's keep track of each date-time range as a pair of ZonedDateTime classes. We could create our own class for the pair, but instead we re-purpose the AbstractMap.SimpleImmutableEntry class. That class tracks a pair of objects as a key and a value.

Do not let the name of that pairing class confuse you. The original intent of that class was to be used with a Map, but is not itself a map. That class is meant to refer to a key and a value from a Map, but we will use in a manner where its “key” is our starting moment and its “value” is our stopping moment.

We collect each calculated time range in a List of that AbstractMap.SimpleImmutableEntry type.

int initialCapacity = ( ( int ) ChronoUnit.HOURS.between ( zdtStart , zdtStop ) ) + 2; // Plus two for good measure. (not sure if needed).
List<AbstractMap.SimpleImmutableEntry<ZonedDateTime , ZonedDateTime>> ranges = new ArrayList<> ( initialCapacity );

Set up our loop with the starting value, and loop. On each loop we want to truncate the value to a whole hour (to lop off seconds and fractional-second). After truncation, we add an hour to get to next hour. We only need that truncation on the first loop, so you could re-write this code to be more efficient by avoiding the needless truncation on successive loops. I would not bother unless you are processing very large numbers of hours.

ZonedDateTime zdt = zdtStart;
while ( zdt.isBefore ( zdtStop ) ) {
    ZonedDateTime zdt2 = zdt.truncatedTo ( ChronoUnit.HOURS ).plusHours ( 1 );  // Truncate to whole hour, and add one.
    if ( zdt2.isAfter ( zdtStop ) ) { // Oops, went too far. Clip this range to end at `zdtStop`.
        zdt2 = zdtStop;
    }
    AbstractMap.SimpleImmutableEntry<ZonedDateTime , ZonedDateTime> range = new AbstractMap.SimpleImmutableEntry<> ( zdt , zdt2 );
    ranges.add ( range );
    // Prepare for next loop.
    zdt = zdt2;
}

Dump to console.

System.out.println ( "ldtStart/ldtStop: " + ldtStart + "/" + ldtStop );
System.out.println ( "zdtStart/zdtStop: " + zdtStart + "/" + zdtStart );
System.out.println ( "ranges: " + ranges );

When run.

ldtStart/ldtStop: 2017-03-07T06:30/2017-03-07T11:35

zdtStart/zdtStop: 2017-03-07T06:30-05:00[America/Montreal]/2017-03-07T06:30-05:00[America/Montreal]

ranges: [2017-03-07T06:30-05:00[America/Montreal]=2017-03-07T07:00-05:00[America/Montreal], 2017-03-07T07:00-05:00[America/Montreal]=2017-03-07T08:00-05:00[America/Montreal], 2017-03-07T08:00-05:00[America/Montreal]=2017-03-07T09:00-05:00[America/Montreal], 2017-03-07T09:00-05:00[America/Montreal]=2017-03-07T10:00-05:00[America/Montreal], 2017-03-07T10:00-05:00[America/Montreal]=2017-03-07T11:00-05:00[America/Montreal], 2017-03-07T11:00-05:00[America/Montreal]=2017-03-07T11:35-05:00[America/Montreal]]


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
  • 303,325
  • 100
  • 852
  • 1,154