ZonedDateTime to Date before Java 8 in early Android
Asked Answered
T

5

11

I am trying to replace the ZonedDateTime.toInstant method because it is only available since API 26 for Android.
But my app is supposed to support API 19.
I want to convert the ZonedDateTime to a Date so i can do something like this:

final Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
final long millis = calendar.getTimeInMillis();

What i want to achieve is the following:
I want to calculate the difference between the current date and another date in Seconds, Minutes, Hours, ... the highest possible unit wins, so i would get e.g. 5 days ago as result.

Triune answered 22/2, 2019 at 14:5 Comment(7)
How are you constructing your ZonedDateTime instance? Why can't you just replace it with the code in your question?Untruthful
Possible duplicate of Difference in days between two dates in Java?Amor
Can't you use Joda Time or the ThreeTen Backport?Sadiras
@Untruthful im getting a string from a json which is then passed to ZonedDateTime.parse(). I will try the ThreeTen Backport and will give an update.Triune
What does the string from JSON look like? Asking because if it’s like 2019-02-23T10:21:04.791797+01:00, then OffsetDateTime is a still better match for parsing. The question is whether the string has a time zone in it (like Pacific/Pitcairn or ACWST) or only an offset (like +01:00).Uniocular
The date string is e.g. "2019-02-23T16:50:21Z", so it's UTC.Triune
I added a solution taking the String instead of a ZonedDateTime. Before that i had to adjust my API library to return the original String instead of the already parsed ZonedDateTime, but it is working! Thank you.Triune
T
1

Solution (ThreeTen-Backport Library):
It's working perfectly, i already tried it out on an KitKat emulator.

private static final ChronoUnit[] chronoUnits = {ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.DAYS, ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS};
private static final Map<ChronoUnit, Integer> chronoUnitPluralIdMap = new HashMap<ChronoUnit, Integer>() {{
    put(ChronoUnit.YEARS, R.plurals.chrono_unit_years_ago);
    put(ChronoUnit.MONTHS, R.plurals.chrono_unit_months_ago);
    put(ChronoUnit.DAYS, R.plurals.chrono_unit_days_ago);
    put(ChronoUnit.HOURS, R.plurals.chrono_unit_hours_ago);
    put(ChronoUnit.MINUTES, R.plurals.chrono_unit_minutes_ago);
    put(ChronoUnit.SECONDS, R.plurals.chrono_unit_seconds_ago);
}};

public static String getTimeStringUntilNowFromUTC(Context context, String utcDate) {
    Instant now = Instant.now(Clock.systemUTC());
    Instant then = Instant.parse(utcDate);
    for (ChronoUnit chronoUnit : chronoUnits) {
        if (then.isSupported(chronoUnit)) {
            long units = chronoUnit.between(then, now);
            if (units > 0) {
                //noinspection ConstantConditions
                return context.getResources().getQuantityString(chronoUnitPluralIdMap.get(chronoUnit), (int)units, (int)units);
            }
        }
    }
    return "-";
}

public static String getTimeBetweenTwoDates(Context context, String date1, String date2) {
    Instant date1Instant = Instant.parse(date1);
    Instant date2Instant = Instant.parse(date2);
    final long seconds = ChronoUnit.SECONDS.between(date1Instant, date2Instant);
    return getMinutesSecondsString(context, seconds);
}
Triune answered 26/2, 2019 at 6:21 Comment(0)
S
10

You can enable "Support for newer Java language APIs" simply by this gradle configuration:

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

android {
  defaultConfig {
    //Only required when setting minSdkVersion to 20 or lower
    multiDexEnabled true
  }

  compileOptions {
    // Flag to enable support for the new language APIs
    coreLibraryDesugaringEnabled true
    // Sets Java compatibility to Java 8
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  // Dependency with the implementation code for the APIs
  coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5'
}

Source: https://medium.com/androiddevelopers/support-for-newer-java-language-apis-bca79fc8ef65

Shirr answered 10/11, 2020 at 16:7 Comment(1)
Tested and it's working! i am using ZonedDateTime in my project which normally works only on api>=26, so i downloaded emulator with api 24 and the project run without crashing!Ventose
C
9

tl;dr

For Android before 26, use the ThreeTen-ABP library.

Avoid legacy date-time classes

The old date-time classes such as Calendar and Date are terrible, really, awful wretched classes. They are laden with bad design decisions and hacks, built by people who did not understand date-time handling. Avoid them. They were entirely supplanted by the java.time classes with the adoption of JSR 310 for a reason – actually, many reasons.

Avoid these legacy classes. Use only java.time classes.

ThreeTen-Backport library

For Java 6 and Java 7, most of the java.time functionality is back-ported in the ThreeTen-Backport project.

ThreeTen-ABP

That back-port is further adapted to earlier Android (<26) in the ThreeTen-ABP project.

I urge you to add this library to your project so you can avoid ever using the tragic legacy classes.

Conversion

Where you need to interface with old code not yet updated to java.time, convert back-and-forth between legacy and modern.

In Java 8 and later, convert by calling the new to… and from… methods found on the old classes.

In the back-port, convert using the to… conversion methods found on the org.threeten.bp.DateTimeUtils class.

Elapsed time

Your Question talks about calculating elapsed time.

To count years, months, and days, use Period.

To count days (24-hour chunks of time unrelated to the calendar), hours, minutes, seconds, and fractional second, use Duration.

Search Stack Overflow for more info. These classes have been covered many time already.


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.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

enter image description here

Cu answered 22/2, 2019 at 16:29 Comment(0)
U
3

There’s already a good answer by Basil Bourque. After your comments I thought I could be just a little more specific:

public static String diff(String thenStr) {
    Instant now = Instant.now();
    Instant then = Instant.parse(thenStr);
    ChronoUnit[] units = ChronoUnit.values();
    // try longest units first, they are in the far end of the array
    for (int i = units.length - 1; i >= 0; i--) {
        if (then.isSupported(units[i])) {
            long diffInCurrentUnit = units[i].between(then, now);
            if (diffInCurrentUnit != 0) {
                return "" + diffInCurrentUnit + ' ' + units[i].toString().toLowerCase();
            }
        }
    }
    return "0";
}

Let’s try it out:

    System.out.println(diff("2019-02-23T16:50:21Z"));
    System.out.println(diff("2019-02-23T20:15:21Z"));

Output when running just now:

3 hours
36 seconds

Imports I used:

import org.threeten.bp.Instant;
import org.threeten.bp.temporal.ChronoUnit;
import org.threeten.bp.temporal.UnsupportedTemporalTypeException;

Instant doesn’t support coarser units than days (at 24 hours). If you need to be able to return weeks, months or years, just use OffsetDateTime instead of Instant.

Question: Can I use java.time on my Android API level?

Yes, java.time works nicely on 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 this case import from java.time with subpackages.
  • In 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 use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

A stream?

EDIT: @Basil Bourque asked in a comment:

I wonder if this could be crunched down into a stream, perhaps a one-liner?

It can, but I don’t think it will be an advantage in this case:

    return IntStream.range(0, units.length)
            .map(i -> units.length - i - 1)
            .mapToObj(i -> units[i])
            .filter(then::isSupported)
            .filter(unit -> unit.between(then, now) != 0)
            .map(unit -> "" + unit.between(then, now) + ' ' + unit.toString().toLowerCase())
            .findFirst()
            .orElse("0");

I find the code for taking the array elements backward, .map(i -> units.length - i - 1), a bit hard to read. We need to calculate the difference twice, first for filtering and then for assembling the string result. But it works, and you may go with it if you like.

The double calculation can be avoided with an inner stream pipeline, again harder to read, though:

            .flatMap(unit -> LongStream.of(unit.between(then, now))
                    .filter(diff -> diff != 0)
                    .mapToObj(diff -> "" + diff + ' ' + unit.toString().toLowerCase()))

Links

Uniocular answered 23/2, 2019 at 20:19 Comment(3)
Interesting approach, looping all ChronoUnit enum values. I wonder if this could be crunched down into a stream, perhaps a one-liner?Cu
It can be turned into a stream, @BasilBourque (and even better after I got rid of the superfluous try-catchin the code). See my edit. I avoided it at first because I found the code for streaming an array backward harder to read (inspired from Java 8 stream reverse order).Uniocular
Thanks for doing that, @ManuelWa. You may consider posting it as your own answer rather than in the question. I did think that you just might not want to consider all chrono units. I see that you found a solution for that.Uniocular
T
1

Solution (ThreeTen-Backport Library):
It's working perfectly, i already tried it out on an KitKat emulator.

private static final ChronoUnit[] chronoUnits = {ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.DAYS, ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS};
private static final Map<ChronoUnit, Integer> chronoUnitPluralIdMap = new HashMap<ChronoUnit, Integer>() {{
    put(ChronoUnit.YEARS, R.plurals.chrono_unit_years_ago);
    put(ChronoUnit.MONTHS, R.plurals.chrono_unit_months_ago);
    put(ChronoUnit.DAYS, R.plurals.chrono_unit_days_ago);
    put(ChronoUnit.HOURS, R.plurals.chrono_unit_hours_ago);
    put(ChronoUnit.MINUTES, R.plurals.chrono_unit_minutes_ago);
    put(ChronoUnit.SECONDS, R.plurals.chrono_unit_seconds_ago);
}};

public static String getTimeStringUntilNowFromUTC(Context context, String utcDate) {
    Instant now = Instant.now(Clock.systemUTC());
    Instant then = Instant.parse(utcDate);
    for (ChronoUnit chronoUnit : chronoUnits) {
        if (then.isSupported(chronoUnit)) {
            long units = chronoUnit.between(then, now);
            if (units > 0) {
                //noinspection ConstantConditions
                return context.getResources().getQuantityString(chronoUnitPluralIdMap.get(chronoUnit), (int)units, (int)units);
            }
        }
    }
    return "-";
}

public static String getTimeBetweenTwoDates(Context context, String date1, String date2) {
    Instant date1Instant = Instant.parse(date1);
    Instant date2Instant = Instant.parse(date2);
    final long seconds = ChronoUnit.SECONDS.between(date1Instant, date2Instant);
    return getMinutesSecondsString(context, seconds);
}
Triune answered 26/2, 2019 at 6:21 Comment(0)
I
1

Answer by Roman Droppa works fine but today (2023) you must change version of library:

 compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    // Dependency with the implementation code for the APIs
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
Inspector answered 27/2, 2023 at 17:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.