1

I have a ZonedDateTime stored in a timeStamp variable.

I also have a Duration barDuration that can be one minute, 5 minutes, 15 minutes, one hour, 6 hours or one day.

I'm plotting a candle chart which width is barDuration.

If the bar duration is set to 1 minute, I need that bars to start at a round minute (e.g. 9:43:00).

If it is set to 5 minutes, I need the bars to start at 9:45, 9:50, 9:55 etc.

If is is set to 15 minutes, then the bars start at 9:45, 10:00, 10:15, etc.

When barDuration is 1 minute, I know I can compute the end time of the bar using:

timeStamp.truncatedTo(ChronoUnit.MINUTES).plus(barDuration)

But this is hard-coded and I want to use the barDuration directly to perform the truncation. How can this be done?

Edit:

I found here a solution with Clocks:

Clock baseClock = Clock.tickMinutes(timeStamp.getZone());
Clock barClock = Clock.tick(baseClock, barDuration);
LocalDateTime now =  LocalDateTime.now(barClock);
LocalDateTime barEndTime =  now.plus(barDuration);

This would work, but it is not based to my timeStamp, but on using LocalDateTime.now(), which is not what I want (I'm not sure the local time on my machine is synchronized with the timestamp I retrieve from a remote server).

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Ben
  • 6,321
  • 9
  • 40
  • 76
  • To see why this is difficult, what would you expect to happen if your duration was "3 minutes, 20 seconds and 7 nanoseconds"? Would you expect it to round relative to the start of the day, or some epoch? – Jon Skeet Feb 24 '18 at 09:07
  • I know this is difficult. But in my case, I only have the listed possibilities for the bar duration. So maybe I have to create all the clocks manually – Ben Feb 24 '18 at 09:10
  • If you're not trying to use the value of "current time" at all, then you shouldn't be using a `Clock`. You should work out what you want to do using `ZonedDateTime` or related types. But I would start *thinking* about how you'd want the code to behave in my weird example - because that's likely to lead you to the implementation you want. (My *guess* is that you'll want to find the number of durations from the start of the day in that time zone, truncate that and multiply it by the duration, then add the result to the start of the day. But that's just a guess.) – Jon Skeet Feb 24 '18 at 09:12
  • JB Nizet's answer raises an important question about how you want this to work - are you sure you need it to work on ZonedDateTime? If you always expect 24 durations of 1 hour per day for example, that's going to cause problems... – Jon Skeet Feb 24 '18 at 09:36

2 Answers2

2

As a humble supplement to JB Nizets answer here’s (EDIT:) a Java 9 version that is more time unit neutral.

static ZonedDateTime truncateToDuration(ZonedDateTime zonedDateTime, Duration duration) {
    ZonedDateTime startOfDay = zonedDateTime.truncatedTo(ChronoUnit.DAYS);
    return startOfDay.plus(duration.multipliedBy(
            Duration.between(startOfDay, zonedDateTime).dividedBy(duration)));
}

I have not tested thoroughly, but I expect it to work with rounding to for example 500 nanos too. I divide the duration since start of day with the duration to round to, and immediately multiply back, just to obtain a whole number of that duration.

Funny results will probably occur if your duration doesn’t divide into an hour, or your day starts on something else than a whole hour (if that occurs at all).

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
1

Maybe something more elegant is possible, but the following seems to be working fine.

Make sure to look at the DST tests, though, because you might find it surprising to have 05:00 and 07:00 as the start of the 6 hours duration. Maybe that's not what you actually want in these cases.

public class TruncationTest {

    static ZonedDateTime truncateToDuration(ZonedDateTime zonedDateTime, Duration duration) {
        ZonedDateTime startOfDay = zonedDateTime.truncatedTo(ChronoUnit.DAYS);
        long millisSinceStartOfDay = zonedDateTime.toInstant().toEpochMilli() - startOfDay.toInstant().toEpochMilli();
        long millisToSubtract = millisSinceStartOfDay % duration.toMillis();
        return zonedDateTime.truncatedTo(ChronoUnit.MILLIS).minus(millisToSubtract, ChronoUnit.MILLIS);
    }

    @Test
    public void test() {
        Duration oneMinute = Duration.of(1, ChronoUnit.MINUTES);
        Duration fiveMinutes = Duration.of(5, ChronoUnit.MINUTES);
        Duration fifteenMinutes = Duration.of(15, ChronoUnit.MINUTES);
        Duration oneHour = Duration.of(1, ChronoUnit.HOURS);
        Duration sixHours = Duration.of(6, ChronoUnit.HOURS);
        Duration oneDay = Duration.of(1, ChronoUnit.DAYS);

        ZoneId zone = ZoneId.systemDefault();
        ZonedDateTime now = LocalDateTime.parse("2018-02-24T10:37:07.123").atZone(zone);

        assertEquals(LocalDateTime.parse("2018-02-24T10:37:00.000").atZone(zone),
                     truncateToDuration(now, oneMinute));
        assertEquals(LocalDateTime.parse("2018-02-24T10:35:00.000").atZone(zone),
                     truncateToDuration(now, fiveMinutes));
        assertEquals(LocalDateTime.parse("2018-02-24T10:30:00.000").atZone(zone),
                     truncateToDuration(now, fifteenMinutes));
        assertEquals(LocalDateTime.parse("2018-02-24T10:00:00.000").atZone(zone),
                     truncateToDuration(now, oneHour));
        assertEquals(LocalDateTime.parse("2018-02-24T06:00:00.000").atZone(zone),
                     truncateToDuration(now, sixHours));
        assertEquals(LocalDateTime.parse("2018-02-24T00:00:00.000").atZone(zone),
                     truncateToDuration(now, oneDay));
    }

    @Test
    public void testOnFirstDST() {
        Duration oneMinute = Duration.of(1, ChronoUnit.MINUTES);
        Duration fiveMinutes = Duration.of(5, ChronoUnit.MINUTES);
        Duration fifteenMinutes = Duration.of(15, ChronoUnit.MINUTES);
        Duration oneHour = Duration.of(1, ChronoUnit.HOURS);
        Duration sixHours = Duration.of(6, ChronoUnit.HOURS);
        Duration oneDay = Duration.of(1, ChronoUnit.DAYS);

        ZoneId zone = ZoneId.of("Europe/Paris");
        ZonedDateTime now = LocalDateTime.parse("2018-03-25T10:37:07.123").atZone(zone);

        assertEquals(LocalDateTime.parse("2018-03-25T10:37:00.000").atZone(zone),
                     truncateToDuration(now, oneMinute));
        assertEquals(LocalDateTime.parse("2018-03-25T10:35:00.000").atZone(zone),
                     truncateToDuration(now, fiveMinutes));
        assertEquals(LocalDateTime.parse("2018-03-25T10:30:00.000").atZone(zone),
                     truncateToDuration(now, fifteenMinutes));
        assertEquals(LocalDateTime.parse("2018-03-25T10:00:00.000").atZone(zone),
                     truncateToDuration(now, oneHour));
        assertEquals(LocalDateTime.parse("2018-03-25T07:00:00.000").atZone(zone),
                     truncateToDuration(now, sixHours));
        assertEquals(LocalDateTime.parse("2018-03-25T00:00:00.000").atZone(zone),
                     truncateToDuration(now, oneDay));
    }

    @Test
    public void testOnSecondDST() {
        Duration oneMinute = Duration.of(1, ChronoUnit.MINUTES);
        Duration fiveMinutes = Duration.of(5, ChronoUnit.MINUTES);
        Duration fifteenMinutes = Duration.of(15, ChronoUnit.MINUTES);
        Duration oneHour = Duration.of(1, ChronoUnit.HOURS);
        Duration sixHours = Duration.of(6, ChronoUnit.HOURS);
        Duration oneDay = Duration.of(1, ChronoUnit.DAYS);

        ZoneId zone = ZoneId.of("Europe/Paris");
        ZonedDateTime now = LocalDateTime.parse("2018-10-28T10:37:07.123").atZone(zone);

        assertEquals(LocalDateTime.parse("2018-10-28T10:37:00.000").atZone(zone),
                     truncateToDuration(now, oneMinute));
        assertEquals(LocalDateTime.parse("2018-10-28T10:35:00.000").atZone(zone),
                     truncateToDuration(now, fiveMinutes));
        assertEquals(LocalDateTime.parse("2018-10-28T10:30:00.000").atZone(zone),
                     truncateToDuration(now, fifteenMinutes));
        assertEquals(LocalDateTime.parse("2018-10-28T10:00:00.000").atZone(zone),
                     truncateToDuration(now, oneHour));
        assertEquals(LocalDateTime.parse("2018-10-28T05:00:00.000").atZone(zone),
                     truncateToDuration(now, sixHours));
        assertEquals(LocalDateTime.parse("2018-10-28T00:00:00.000").atZone(zone),
                     truncateToDuration(now, oneDay));
    }
}
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • I'd avoid using millis with the new java.time APIs, on general principle - the inherent precision is nanoseconds. While the OP's use case is for durations of minutes, this code would fail with a duration of (say) 500 nanoseconds, which someone else using the answer might want. – Jon Skeet Feb 24 '18 at 09:27
  • @JonSkeet Supporting nanoseconds, AFAIK, would meke the code more complex (since you can't just ask for nanos since the epoch), and would be of no use for the OP. I did truncate to nanoseconds to make sure they're always zero. I'll leave it up to future readers to read the question and the answer correctly, and to avoid blindly using the code without even testing it for their use-case. But I would be happy to discover a nicer solution than this one. – JB Nizet Feb 24 '18 at 09:51
  • It really depends on whether the Unix epoch is what the OP wants to round towards, really - I haven't added an answer as the question isn't clear enough yet IMO. – Jon Skeet Feb 24 '18 at 10:20