0

I'm unsure whether this can be made more efficient or not, but I need to get the number of days that have passed since a unix/epoch timestamp, where the time itself is not a factor, only the date compared to now.

Example 1: 
Timestamp is : 3rd September 14:35 
Compared to now which is: 4th September 00:35 
Days difference = 1

Example 2:
Timestamp is: 3rd September 23:55
Compared to now which is: 4th September 00:35
Days difference = 1

Example 3:
Timestamp is: 2nd September 02:23
Compared to now which is: 4th September 00:35
Days difference = 2

To get this, I have the following code:

String epoch = "1599134401" // the unix/epoch timestamp in seconds
Long epochMillis = Long.valueOf(epoch) * 1000;
Date epochDateObj = new Date(epochMillis);
Calendar tsCal = Calendar.getInstance();
tsCal.setTime(epochDateObj);
tsCal.set(Calendar.HOUR_OF_DAY, 0);
tsCal.set(Calendar.MINUTE, 0);
tsCal.set(Calendar.SECOND, 0);
tsCal.set(Calendar.MILLISECOND, 0);

Calendar today = Calendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.SECOND, 0);
today.set(Calendar.MILLISECOND, 0);

long diffInMillies = Math.abs(today.getTime().getTime() - tsCal.getTime().getTime());
long diff = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS);
if(diff > 1) {
    return diff + " days";
} else {
    return diff + " day";
}

The above code works, but to me, it seems quite elaborate for such a rather small thing as this.

Any suggestions for optimizing it? Maybe there's some functionality I don't know about. Its an Android app which is using a rather old SDK (back to Android 4.1).

colourCoder
  • 1,394
  • 2
  • 11
  • 18
Daniel Jørgensen
  • 1,183
  • 2
  • 19
  • 42
  • I recommend you don’t use `Date` and `Calendar`. Those classes are poorly designed and long outdated. Instead use `Instant`, `LocalDate`, `ZoneId` and `ChronoUnit`, all from [java.time, the modern Java date and time API](https://docs.oracle.com/javase/tutorial/datetime/). – Ole V.V. Sep 04 '20 at 05:46
  • In which time zone do you want to count? For starters, the Unix epoch fell on December 31, 1969 in some time zones, on January 1, 1970 in others. – Ole V.V. Sep 04 '20 at 05:48

3 Answers3

1

Days are quite fundamentally a human concept. They involve politics, opinion, confusion, timezones, eras, epochs, and other very hairy concepts. java.util.Date has no snowball's chance in hades to do it right. Nor does calendar.

Your only hope is a proper API, such as java.time.

Furthermore you need to clean up your question. What you're asking is impossible; you're comparing guns to grandmas. epoch-time is fundamentally a 'computer' concept - it refers solely to moments in time, it has no idea when, where, who, which political party, etc you are asking. Which is a problem, because without any of that information it is NOT possible to know what day it is. Seconds are more or less universal, but days are not. A day can be 23 hours or 25 hours, or 23 hours, 59 minutes and 59 seconds, or 24 hours and 1 second - sometimes whole days get skipped, etcetera. 'how long is a day' is not answerable without knowing who you ask and what timezone (and sometimes, political entity!) is used as context.

So, let's say you're asking someone in arizona. The answer will then depend rather a lot on where in arizona you ask and who you ask: You would need to (potentially) know whether the person you so happen to ask applies daylight savings time or not back in 1970 as well as in the 'target' time. This depends on whether you're asking when you're on an native american reservation within arizona or not, and/or if the person you're asking is sticking to NAR zones or not. Hence why I mentioned the politics thing, and why what you want is completely impossible.

java.time to the rescue which can actually represent the crazy mess!

Instant represents a moment in time. It's internally stored as epoch-millis and cannot tell you the day, month, year, era, hour, etc of that moment in time by itself. That's because.. well, that's because that's how reality works. If I snap my fingers right now, and I ask someone 'what time is it', it depends on where I am and where the person I'm asking is and what political parties they ascribe to, so it's not possible. But, you combine a Zone and an Instant and now we're getting somewhere.

LocalDateTime represents a time as a human would say it: A year/month/day + hour/minute/second. It is not possible to turn this into epochmillis for the same reason in reverse. And for the same reason, if you combine this with a Zone doors start opening.

ZonedDateTime tries to bridge the gap: It represents a time as a human would say it, but we code in the location (and political affiliations) of the human who said it. You can store this either as a LocalDateTime + TimeZone, or as an Instant+TimeZone (you don't need to know how it is implemented, of course). You can move from a ZDT to either Instant or LocalDateTime, of course, and this one can answer many questions.

Let's try to solve your problem:

String epoch = "1599134401"; // the unix/epoch timestamp in seconds
String where = "Europe/Berlin"; // what you want is impossible without this!!

Instant instant = Instant.ofEpochSecond(Long.valueOf(epoch));
ZonedDateTime target = instant.atZone(ZoneId.of(where));
ZonedDateTime today = ZonedDateTime.now(where);

long days = ChronoUnit.DAYS.between(target, today);
System.out.println(days);

As a general rule, if you start doing serious math on dates you're messing up and it won't work. Not that your tests will ever catch it of course; it'll go ape when the clocks go back or forward or some political party decides 5 days before it happens to end daylight savings time, or the client is in one place and your server is in another, etc - all stuff that tests rarely catch.

Proper use of java.time should usually mean you aren't doing much calculation, and so it is here, fortunately.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
0

There’s already a very great and insightful answer by rzwitserloot, I highly recommend it. Just as a minor supplement I wanted to give you my go at the code. Still using java.time, the modern Java date and time API, of course.

    ZoneId zone = ZoneId.of("Europe/Tirane");
    DateTimeFormatter epochSecondFormatter = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.INSTANT_SECONDS)
            .toFormatter();
    
    String epoch = "1599134401"; // the unix/epoch timestamp in seconds
    Instant then = epochSecondFormatter.parse(epoch, Instant::from);
    LocalDate thatDay = then.atZone(zone).toLocalDate();
    
    LocalDate today = LocalDate.now(zone);
    
    long diff = ChronoUnit.DAYS.between(thatDay, today);
    diff = Math.abs(diff);
    if (diff == 1) {
        System.out.println("" + diff + " day");
    } else {
        System.out.println("" + diff + " days");
    }

When I ran the code just now, the output was:

1 day

Since you want to ignore the time of day, LocalDate is the correct class to use for the dates. A LocalDate is a date with time of day and without time zone.

In English (not being a native speaker, though) I prefer saying “0 days”, not “0 day”. So I have changed your condition for choosing between singular and plural.

Did your code work?

Your code gives inaccurate results in corner cases. TimeUnit is generally a fine enum for time unit conversions, but it assumes that a day is always 24 hours, which is not always the case, as rzwitserloot explained. The java.time code of that answer and of this one correctly takes transitions to and from summer time (DST) and other time anomalies into account.

Question: Does java.time work on Android 4.1?

java.time works nicely on both older and newer Android devices. It just requires at least Java 6.

  • In Java 8 and later and on newer Android devices (from API level 26) the modern API comes built-in.
  • In non-Android Java 6 and 7 get the ThreeTen Backport, the backport of the modern classes (ThreeTen for JSR 310; see the links at the bottom).
  • On older Android either use desugaring or the Android edition of ThreeTen Backport. It’s called ThreeTenABP. In the latter case make sure you import the date and time classes from org.threeten.bp with subpackages.

Links

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

If we don't want to add the ThreeTenABP library to our project, we need to normalize to a date-without-time in UTC, in order to prevent things like Daylight Savings Time to skew the results.

For that, a helper method is appropriate:

static long toDateUtcMillis(Date time) {
    // Get year/month/day according to default time zone
    Calendar cal = Calendar.getInstance();
    cal.setTime(time);
    int year = cal.get(Calendar.YEAR);
    int month = cal.get(Calendar.MONTH);
    int day = cal.get(Calendar.DAY_OF_MONTH);
    
    // Set year/month/day in UTC
    cal.setTimeZone(TimeZone.getTimeZone("UTC"));
    cal.clear();
    cal.set(year, month, day);
    return cal.getTimeInMillis();
}

We can now easily calculate the number of days. In the following we return negative value if the dates are reverse. Add call to Math.abs() if that's not desired.

static int daysBetween(Date date1, Date date2) {
    long dateMillis1 = toDateUtcMillis(date1);
    long dateMillis2 = toDateUtcMillis(date2);
    return (int) TimeUnit.MILLISECONDS.toDays(dateMillis2 - dateMillis1);
}

Test

public static void main(String[] args) throws Exception {
    test("3 September 2020 14:35", "4 September 2020 00:35");
    test("3 September 2020 23:55", "4 September 2020 00:35");
    test("2 September 2020 02:23", "4 September 2020 00:35");
}

static void test(String date1, String date2) throws ParseException {
    // Parse the date strings in default time zone
    SimpleDateFormat format = new SimpleDateFormat("d MMMM yyyy HH:mm", Locale.US);
    int days = daysBetween(format.parse(date1), format.parse(date2));
    System.out.println("Timestamp is: " + date1);
    System.out.println("Compared to: " + date2);
    System.out.println("Days difference = " + days);
    System.out.println();
}

Output

Timestamp is: 3 September 2020 14:35
Compared to: 4 September 2020 00:35
Days difference = 1

Timestamp is: 3 September 2020 23:55
Compared to: 4 September 2020 00:35
Days difference = 1

Timestamp is: 2 September 2020 02:23
Compared to: 4 September 2020 00:35
Days difference = 2

Andreas
  • 154,647
  • 11
  • 152
  • 247
  • *we need to normalize to a date-without-time in UTC* — yes, if we want to count days in UTC. – Ole V.V. Sep 04 '20 at 18:36
  • @OleV.V. Days are days, except that sometimes DST makes days be 23 or 25 hours long, instead of the common 24 hours, so we need a time zone without DST in order for millisecond calculations not to go wrong. UTC is such a time zone. It has nothing to do with UTC, it's all about DST-avoidance. If you look at the code, it is taking the local date and *making* a UTC date from it. It is not doing a time zone shift, so we're not *"counting days in UTC"*, we are counting the number of 24-hour periods in the DST-free time zone. – Andreas Sep 05 '20 at 04:16
  • In my view you are adjusting the requirements to your code where we ought to do it the other way around … – Ole V.V. Sep 05 '20 at 04:18
  • 1
    @OleV.V. I wrote that code for this question, which is about comparing two *"timestamps, where the time itself is not a factor, only the date is compared"*. E.g. I'm in Eastern Time Zone, and the number of days between 2020-10-31T00:00 and 2020-11-01T23:59 is **1 day**, since there is one day between Oct 31 and Nov 1, even though there are 48 hours and 59 minutes between the two timestamps, i.e. more than 2 days as counting in milliseconds, because DST ends on Sunday, November 1 at 2:00 AM. – Andreas Sep 05 '20 at 04:26
  • Daniel, which result would you want to have in the example mentioned in Andreas’ last comment, 1 day or 2 days? – Ole V.V. Sep 06 '20 at 11:29