-2

I'm trying to round a java.sql.Timestamp to the nearest day. All the timestamps are in UTC time.

At this location https://stackoverflow.com/a/60100403/9745488, I saw a way to get to the hour, but I can't seem to figure it out.

The 1800000 seems to be 30 minutes in milliseconds, so for me 12 hours would be 43200000ms. The 60000 * 60 would seem to be 1 hour in milliseconds. I tried doing (600006024), but the results just don't come out right. How to do this rounding?

public class SqlTimestamp {
 
 public static void main(String...strings) {
  Timestamp[] timestamps = {
    new Timestamp(Timestamp.valueOf("2023-02-19 00:00:00.000000000").getTime()),
    new Timestamp(Timestamp.valueOf("2023-02-25 23:59:59.999999999").getTime()),
    new Timestamp(Timestamp.valueOf("2023-02-25 12:00:00.000000000").getTime()),
    new Timestamp(Timestamp.valueOf("2023-02-25 12:29:00.000000000").getTime()),
    new Timestamp(Timestamp.valueOf("2023-02-25 12:30:00.000000000").getTime()),
    new Timestamp(Timestamp.valueOf("2023-02-25 12:29:59.999999999").getTime()),
  };
  
  for(int i = 0; i < timestamps.length; i++) {
   System.out.println("Original timestamp value=" + timestamps[i].toString());
   System.out.println("round to nearest hour=" +   Timestamp.from(Instant.ofEpochMilli(((timestamps[i].getTime()+1800000)/(60000*60))*(60000*60)))   );
   
   System.out.println("round to nearest day=" +   Timestamp.from(Instant.ofEpochMilli((((timestamps[i].getTime()+43200000)/(60000*60*24))*(60000*60*24))   )  )    );
  }
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Oxnard
  • 237
  • 1
  • 6
  • 18
  • timestamp.toLocalDateTime().toLocalDate() ? just a guess, I have not tried it. – undefined symbol Feb 28 '23 at 23:43
  • @undefinedsymbol that's the right general idea, but it would always round downwards. So you'll need to add 12 hours, somewhere along the way. Oh, and you can do the same thing with the `truncatedTo` method. – Dawood ibn Kareem Feb 28 '23 at 23:56
  • 2
    Do you really need to use Timestamp? – shmosel Mar 01 '23 at 01:47
  • 1
    @DawoodIbnKareem dangerously bad advice, you can't treat date/times with that cavalier attitude, you get bugs that tests don't catch. Trivial example: On half of all daylight savings switchover days, 'add 12 hours then round' is __WRONG__ for about half an hour. – rzwitserloot Mar 01 '23 at 02:55
  • Well, that depends what you mean by "wrong". I guess OP needs to decide whether they want the cutover to always happen at 12:00pm, or whether they want the cutover to happen halfway between midnight today and midnight tomorrow. – Dawood ibn Kareem Mar 01 '23 at 03:04
  • By what time zone do you want to determine the date? For any given moment, the date varies around the globe by time zone. (I'm voting to close for lack of details.) – Basil Bourque Mar 01 '23 at 03:04
  • @DawoodibnKareem Unfortunately, the OP has not detailed their requirements. So we cannot answer this Question precisely. I suggest voting to close. – Basil Bourque Mar 01 '23 at 03:06
  • FYI… The `java.sql.Timestamp` class is one of the terrible legacy date-time classes that were years ago supplanted by the modern *java.time* classes defined in JSR 310. – Basil Bourque Mar 01 '23 at 03:07
  • @BasilBourque or you could write an answer that makes your assumptions clear - like whether they mean "nearest UTC day" or "nearest local day", and how they want to handle the dates when daylight savings starts and ends. – Dawood ibn Kareem Mar 01 '23 at 03:08

2 Answers2

3

The central problem is this: A java.sql.Timestamp object represents an instant in time without timezone information.

Meaning, what you want is utterly impossible - your timestamp object just tracks a moment in time, and at some moment in time, it was January 5th 2022 in one locale, and january 6th in another. 'some moment in time' doesn't translate to a date. Not without localizing it to some timezone.

Given that it is java.sql.Timestamp, 2 things seem obvious:

  • You got this from a database.
  • Likely that database represents some date/time reckoning where 'round to the nearest day even makes sense' (TIMESTAMP WITHOUT TIME ZONE certainly would not!!), and you used .getTimestamp() to get this value, which is incorrect - that's a common mistake, because the entire java.util.Date hierarchy of classes are all incorrect and badly named (for example, Date represents a moment in time and not a date at all. Simply read the deprecated notice on e.g. .getYear() for some proof).

This means what you need to really do is read the data out of the database properly. getDate and getTimestamp are all obsolete methods you should never use - because the types they return are broken. The right call is something like .getObject(idx, LocalDate.class), or .getObject(idx, ZonedDateTime.class). The JDBC spec (v5) guarantees that this will work, but best thing to do here is to check what your database types actually are, read up on what they actually represent, then pick the right type from the java.time package, then try it to confirm this works.

If for some reason you're stuck with legacy code, then know that political timezone changes are GOING to happen and WILL break stuff (such as the most recent JDKs dropping pre-1970s support in the timezone files because tzdata project decided to do that, or countries simply switching timezones.. Which all of mainland europe is going to soon, so this isn't some 'yeah yeah never gonna happen' hypothetical!) - be prepare for weirdo errors where all of a sudden your 'nearest date' rounder starts rounding 11:20 in the morning up when you expected it to round down, that sort of thing.

Nevertheless, best to write as clean as you can, so, take your obsolete instant in time and turn it into the non-obsolete data type to represent instants in time, and from there, do your conversions:

Instant i = timestamp.toInstant(); // move away from obsolete types
ZonedDateTime zdt = i.atZone(ZoneId.of("Europe/Amsterdam");

// you'll need to know what the timezone is that is represented
// with the timestamp, as the timestamp type doesn't know.
// That, or, do as I said and avoid java.sql.Timestamp in the first place.

LocalDateTime ldt = zdt.toLocalDateTime();

// now, what does 'round' mean? Days do not neccessarily have 24 hours.
// They can have 23, 25, or in rare cases, even weirder values.
// Perhaps it is more sensible to say, 12 noon and up rounds up,
// instead of going for e.g. 11:30 or 12:30 on daylight savings switch days.

LocalDate answer = ldt.toLocalDate();
if (ldt.getHour() > 11) answer = answer.plusDays(1);
return answer;

If you actually want 'nearest', even though that is kinda weird, there is no baked in method to do this. Remaining in ZonedDateTime land, truncate to the nearest day, add a day, take the average between the two, then check whether you're past this midpoint or not to know if you need to add a day.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Haha, did you just post an answer that's equivalent to what you described in a comment as "dangerously bad advice"? (Upvoted for making me smile) – Dawood ibn Kareem Mar 01 '23 at 03:09
  • good point about the daylight savings .. As it so happens the app makes the assumption that all time is in UTC thus did not mention it. – Oxnard Mar 01 '23 at 21:45
  • @Oxnard If you add that to the question - i.e., that all the timestamps are UTC, and that you're interested only in the closest UTC date, rounding down before 12:00 UTC and up after 12:00 UTC - then it will remove the ambiguities that have stopped people from answering, and your question might get reopened. If that happens, it's possible you'll get an answer using `Instant` instead of `LocalDate` and `LocalDateTime`, which is (in my opinion) the correct approach when everything's UTC. – Dawood ibn Kareem Mar 01 '23 at 23:31
  • There is no JDBC v5 specification. The latest JDBC specification is JDBC 4.3. – Mark Rotteveel Mar 04 '23 at 08:54
  • Good catch, @MarkRotteveel - turns out I'm misremembering; it's JDBCv4 spec (or was it v4.2?) that requires java.time types to be supported in `.getObject()`. – rzwitserloot Mar 04 '23 at 15:35
  • The `java.time` support was introduced with JDBC 4.2 (together with Java 8). – Mark Rotteveel Mar 04 '23 at 17:48
-1

I think this might be the sort of thing you're after for rounding to the nearest day:

import java.sql.Timestamp;
import java.time.*;
import java.time.temporal.*;

public class SqlTimestamp {

    public static void main(String... strings) {
        Timestamp[] timestamps = {
                Timestamp.valueOf("2023-02-19 00:00:00.000000000"),
                Timestamp.valueOf("2023-02-25 23:59:59.999999999"),
                Timestamp.valueOf("2023-02-25 11:00:00.000000000"),
                Timestamp.valueOf("2023-02-25 12:29:00.000000000"),
                Timestamp.valueOf("2023-02-25 12:30:00.000000000"),
                Timestamp.valueOf("2023-02-25 12:29:59.999999999")
        };

        for (int i = 0; i < timestamps.length; i++) {
            Instant inst = Instant.ofEpochMilli(timestamps[i].getTime());
            Instant startDay = inst.truncatedTo(ChronoUnit.DAYS);
            Instant nextDay = startDay.plus(1, ChronoUnit.DAYS);
            Instant roundValue = nextDay;
            if (inst.toEpochMilli() - startDay.toEpochMilli() < nextDay.toEpochMilli() - inst.toEpochMilli()) {
                roundValue = startDay;
            }
            System.out.printf("Start: %s, next: %s. Instant %s rounds to %s%n", startDay, nextDay, inst, roundValue);
        }
    }
}
g00se
  • 3,207
  • 2
  • 5
  • 9
  • 1
    This doesn't work - it conflates human reckoning with computer reckoning. Instants don't do days. Period (heh), days are a human concept. First convert to some appropriate human reckoning type (LocalDateTime or ZonedDateTime seem appropriate here), and then do the human math (such as 'round to nearest day') from there. – rzwitserloot Mar 01 '23 at 02:56
  • *Instants don't do days* If that assertion is correct, then there's no hope as the 'ur datum' ( `Timestamp`) is already an 'instant' and moving that into some other form of "human reckoning type" isn't going to help. – g00se Mar 01 '23 at 10:32
  • It does help - the missing information (which time zone) can then be added. At the very least the code makes clear that timestamp info is coming from A and timezone is coming from B and here they are being explicitly mixed together (at `.atZone(theZone)`. – rzwitserloot Mar 01 '23 at 12:29
  • I might be missing something but I don't see timezones mentioned at all. – g00se Mar 01 '23 at 13:09
  • @g00se Although `java.sql.Timestamp` is built on `java.util.Date`, and so *in theory* it is a instant-like thing, in practice given the definition in JDBC that it matches the datetime as stored in the database using the default local JVM timezone, it is in fact a rather floating definition and more like a local datetime. For example, retrieving a (timezone-less) `TIMESTAMP` value of 2023-03-03 23:50 with JVMs in different timezones will produces different instants and dates between 2023-03-03 and 2023-03-04 if derived from the instant, but they all have the same local time value. – Mark Rotteveel Mar 04 '23 at 08:59
  • @MarkRotteveel *For example, retrieving a (timezone-less) TIMESTAMP value of 2023-03-03 23:50 with JVMs in different timezones will produces different instants and dates between 2023-03-03 and 2023-03-04 if derived from the instant* Yes, but that's in the nature of an instant measured from the epoch itself isn't it? I'd be interested to see any source documentation on *given the definition in JDBC that it matches the datetime as stored in the database using the default local JVM timezone* (though I'm not quite sure what you mean there) – g00se Mar 04 '23 at 10:13
  • 1
    I mean that if you have a database with a SQL `TIMESTAMP` 2023-03-03 23:50 and you retrieve it in a JVM with default time zone Europe/Amsterdam, you get an instant equivalent of 2023-03-03 22:50 UTC, with Europe/London, you get 2023-03-03 23:50 UTC, with America/New_York, you get 2023-03-04 04:50 UTC from the same value. The definition in JDBC is not explicit (the closest you'll get is a statement in `PreparedStatement.setTimestamp(int, Calendar)`: _"If no Calendar object is specified, the driver uses the default timezone, which is that of the virtual machine running the application."_ – Mark Rotteveel Mar 04 '23 at 10:22
  • The behaviour of `java.sql.Timestamp.parse(String)` also suggests this. See also [my answer here](https://stackoverflow.com/a/14070771/466862) – Mark Rotteveel Mar 04 '23 at 10:22
  • I think if I have the time, I'll do some experiments with this. e.g. if I have, let's say, a MySql `timestamp` column, what you're saying is that `ResultSet.getTimestamp` is going to produce a different value based on the value of `user.timezone`? – g00se Mar 04 '23 at 10:43
  • @g00se It has to. After all, `j.s.Timestamp`'s fields represent millis-since-epoch __and that is all__ (well, an int field with nanos, which doesn't change what it represents). No timezone field. So, if the DBN is storing y/m/d/h/m/s info, and you read that into a `j.s.Timestamp` object, somebody (DB, or JDBC) __must have__ a timezone to go from one to the other. – rzwitserloot Mar 04 '23 at 18:47
  • This is why `.getTimestamp` should be __banned__ in your linting tools. `.getObject()` with the right j.t type is how to do it. Even if truly it really is a millis-since-epoch (`java.time.Instant`-esque) thing - still better to do `.getObject(idx/colName, Instant.class)` and thus alleviate the considerable baggage that `.getTimestamp` brings to the table. – rzwitserloot Mar 04 '23 at 18:49
  • Yes I'm beginning to see how this 'extra processing' is in reality potentially problematic. Of course we need to consider what happened on the insert too. Have you written up a 'best practice' anywhere btw? – g00se Mar 05 '23 at 09:22